From b453d3ee377e513da7fdf5f9241e0be088489717 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 02:47:53 -0400 Subject: [PATCH 001/152] Move all Wasm related code into ws_js.go This way we don't pollute the directory tree. --- accept_js.go | 20 ---- close.go | 205 +++++++++++++++++++++++++++++++++++ close_notjs.go | 211 ------------------------------------ compress.go | 180 +++++++++++++++++++++++++++++++ compress_notjs.go | 181 ------------------------------- conn.go | 264 +++++++++++++++++++++++++++++++++++++++++++++ conn_notjs.go | 265 ---------------------------------------------- ws_js.go | 134 +++++++++++++++++++++++ 8 files changed, 783 insertions(+), 677 deletions(-) delete mode 100644 accept_js.go delete mode 100644 close_notjs.go delete mode 100644 compress_notjs.go delete mode 100644 conn_notjs.go diff --git a/accept_js.go b/accept_js.go deleted file mode 100644 index daad4b79..00000000 --- a/accept_js.go +++ /dev/null @@ -1,20 +0,0 @@ -package websocket - -import ( - "errors" - "net/http" -) - -// AcceptOptions represents Accept's options. -type AcceptOptions struct { - Subprotocols []string - InsecureSkipVerify bool - OriginPatterns []string - CompressionMode CompressionMode - CompressionThreshold int -} - -// Accept is stubbed out for Wasm. -func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { - return nil, errors.New("unimplemented") -} diff --git a/close.go b/close.go index 7cbc19e9..d76dc2f4 100644 --- a/close.go +++ b/close.go @@ -1,8 +1,16 @@ +// +build !js + package websocket import ( + "context" + "encoding/binary" "errors" "fmt" + "log" + "time" + + "nhooyr.io/websocket/internal/errd" ) // StatusCode represents a WebSocket status code. @@ -74,3 +82,200 @@ func CloseStatus(err error) StatusCode { } return -1 } + +// Close performs the WebSocket close handshake with the given status code and reason. +// +// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for +// the peer to send a close frame. +// All data messages received from the peer during the close handshake will be discarded. +// +// The connection can only be closed once. Additional calls to Close +// are no-ops. +// +// The maximum length of reason must be 125 bytes. Avoid +// sending a dynamic reason. +// +// Close will unblock all goroutines interacting with the connection once +// complete. +func (c *Conn) Close(code StatusCode, reason string) error { + return c.closeHandshake(code, reason) +} + +func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + writeErr := c.writeClose(code, reason) + closeHandshakeErr := c.waitCloseHandshake() + + if writeErr != nil { + return writeErr + } + + if CloseStatus(closeHandshakeErr) == -1 { + return closeHandshakeErr + } + + return nil +} + +var errAlreadyWroteClose = errors.New("already wrote close") + +func (c *Conn) writeClose(code StatusCode, reason string) error { + c.closeMu.Lock() + wroteClose := c.wroteClose + c.wroteClose = true + c.closeMu.Unlock() + if wroteClose { + return errAlreadyWroteClose + } + + ce := CloseError{ + Code: code, + Reason: reason, + } + + var p []byte + var marshalErr error + if ce.Code != StatusNoStatusRcvd { + p, marshalErr = ce.bytes() + if marshalErr != nil { + log.Printf("websocket: %v", marshalErr) + } + } + + writeErr := c.writeControl(context.Background(), opClose, p) + if CloseStatus(writeErr) != -1 { + // Not a real error if it's due to a close frame being received. + writeErr = nil + } + + // We do this after in case there was an error writing the close frame. + c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) + + if marshalErr != nil { + return marshalErr + } + return writeErr +} + +func (c *Conn) waitCloseHandshake() error { + defer c.close(nil) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + err := c.readMu.lock(ctx) + if err != nil { + return err + } + defer c.readMu.unlock() + + if c.readCloseFrameErr != nil { + return c.readCloseFrameErr + } + + for { + h, err := c.readLoop(ctx) + if err != nil { + return err + } + + for i := int64(0); i < h.payloadLength; i++ { + _, err := c.br.ReadByte() + if err != nil { + return err + } + } + } +} + +func parseClosePayload(p []byte) (CloseError, error) { + if len(p) == 0 { + return CloseError{ + Code: StatusNoStatusRcvd, + }, nil + } + + if len(p) < 2 { + return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) + } + + ce := CloseError{ + Code: StatusCode(binary.BigEndian.Uint16(p)), + Reason: string(p[2:]), + } + + if !validWireCloseCode(ce.Code) { + return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) + } + + return ce, nil +} + +// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// and https://tools.ietf.org/html/rfc6455#section-7.4.1 +func validWireCloseCode(code StatusCode) bool { + switch code { + case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: + return false + } + + if code >= StatusNormalClosure && code <= StatusBadGateway { + return true + } + if code >= 3000 && code <= 4999 { + return true + } + + return false +} + +func (ce CloseError) bytes() ([]byte, error) { + p, err := ce.bytesErr() + if err != nil { + err = fmt.Errorf("failed to marshal close frame: %w", err) + ce = CloseError{ + Code: StatusInternalError, + } + p, _ = ce.bytesErr() + } + return p, err +} + +const maxCloseReason = maxControlPayload - 2 + +func (ce CloseError) bytesErr() ([]byte, error) { + if len(ce.Reason) > maxCloseReason { + return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) + } + + if !validWireCloseCode(ce.Code) { + return nil, fmt.Errorf("status code %v cannot be set", ce.Code) + } + + buf := make([]byte, 2+len(ce.Reason)) + binary.BigEndian.PutUint16(buf, uint16(ce.Code)) + copy(buf[2:], ce.Reason) + return buf, nil +} + +func (c *Conn) setCloseErr(err error) { + c.closeMu.Lock() + c.setCloseErrLocked(err) + c.closeMu.Unlock() +} + +func (c *Conn) setCloseErrLocked(err error) { + if c.closeErr == nil { + c.closeErr = fmt.Errorf("WebSocket closed: %w", err) + } +} + +func (c *Conn) isClosed() bool { + select { + case <-c.closed: + return true + default: + return false + } +} diff --git a/close_notjs.go b/close_notjs.go deleted file mode 100644 index 4251311d..00000000 --- a/close_notjs.go +++ /dev/null @@ -1,211 +0,0 @@ -// +build !js - -package websocket - -import ( - "context" - "encoding/binary" - "errors" - "fmt" - "log" - "time" - - "nhooyr.io/websocket/internal/errd" -) - -// Close performs the WebSocket close handshake with the given status code and reason. -// -// It will write a WebSocket close frame with a timeout of 5s and then wait 5s for -// the peer to send a close frame. -// All data messages received from the peer during the close handshake will be discarded. -// -// The connection can only be closed once. Additional calls to Close -// are no-ops. -// -// The maximum length of reason must be 125 bytes. Avoid -// sending a dynamic reason. -// -// Close will unblock all goroutines interacting with the connection once -// complete. -func (c *Conn) Close(code StatusCode, reason string) error { - return c.closeHandshake(code, reason) -} - -func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { - defer errd.Wrap(&err, "failed to close WebSocket") - - writeErr := c.writeClose(code, reason) - closeHandshakeErr := c.waitCloseHandshake() - - if writeErr != nil { - return writeErr - } - - if CloseStatus(closeHandshakeErr) == -1 { - return closeHandshakeErr - } - - return nil -} - -var errAlreadyWroteClose = errors.New("already wrote close") - -func (c *Conn) writeClose(code StatusCode, reason string) error { - c.closeMu.Lock() - wroteClose := c.wroteClose - c.wroteClose = true - c.closeMu.Unlock() - if wroteClose { - return errAlreadyWroteClose - } - - ce := CloseError{ - Code: code, - Reason: reason, - } - - var p []byte - var marshalErr error - if ce.Code != StatusNoStatusRcvd { - p, marshalErr = ce.bytes() - if marshalErr != nil { - log.Printf("websocket: %v", marshalErr) - } - } - - writeErr := c.writeControl(context.Background(), opClose, p) - if CloseStatus(writeErr) != -1 { - // Not a real error if it's due to a close frame being received. - writeErr = nil - } - - // We do this after in case there was an error writing the close frame. - c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) - - if marshalErr != nil { - return marshalErr - } - return writeErr -} - -func (c *Conn) waitCloseHandshake() error { - defer c.close(nil) - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() - - err := c.readMu.lock(ctx) - if err != nil { - return err - } - defer c.readMu.unlock() - - if c.readCloseFrameErr != nil { - return c.readCloseFrameErr - } - - for { - h, err := c.readLoop(ctx) - if err != nil { - return err - } - - for i := int64(0); i < h.payloadLength; i++ { - _, err := c.br.ReadByte() - if err != nil { - return err - } - } - } -} - -func parseClosePayload(p []byte) (CloseError, error) { - if len(p) == 0 { - return CloseError{ - Code: StatusNoStatusRcvd, - }, nil - } - - if len(p) < 2 { - return CloseError{}, fmt.Errorf("close payload %q too small, cannot even contain the 2 byte status code", p) - } - - ce := CloseError{ - Code: StatusCode(binary.BigEndian.Uint16(p)), - Reason: string(p[2:]), - } - - if !validWireCloseCode(ce.Code) { - return CloseError{}, fmt.Errorf("invalid status code %v", ce.Code) - } - - return ce, nil -} - -// See http://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number -// and https://tools.ietf.org/html/rfc6455#section-7.4.1 -func validWireCloseCode(code StatusCode) bool { - switch code { - case statusReserved, StatusNoStatusRcvd, StatusAbnormalClosure, StatusTLSHandshake: - return false - } - - if code >= StatusNormalClosure && code <= StatusBadGateway { - return true - } - if code >= 3000 && code <= 4999 { - return true - } - - return false -} - -func (ce CloseError) bytes() ([]byte, error) { - p, err := ce.bytesErr() - if err != nil { - err = fmt.Errorf("failed to marshal close frame: %w", err) - ce = CloseError{ - Code: StatusInternalError, - } - p, _ = ce.bytesErr() - } - return p, err -} - -const maxCloseReason = maxControlPayload - 2 - -func (ce CloseError) bytesErr() ([]byte, error) { - if len(ce.Reason) > maxCloseReason { - return nil, fmt.Errorf("reason string max is %v but got %q with length %v", maxCloseReason, ce.Reason, len(ce.Reason)) - } - - if !validWireCloseCode(ce.Code) { - return nil, fmt.Errorf("status code %v cannot be set", ce.Code) - } - - buf := make([]byte, 2+len(ce.Reason)) - binary.BigEndian.PutUint16(buf, uint16(ce.Code)) - copy(buf[2:], ce.Reason) - return buf, nil -} - -func (c *Conn) setCloseErr(err error) { - c.closeMu.Lock() - c.setCloseErrLocked(err) - c.closeMu.Unlock() -} - -func (c *Conn) setCloseErrLocked(err error) { - if c.closeErr == nil { - c.closeErr = fmt.Errorf("WebSocket closed: %w", err) - } -} - -func (c *Conn) isClosed() bool { - select { - case <-c.closed: - return true - default: - return false - } -} diff --git a/compress.go b/compress.go index 80b46d1c..63d961b4 100644 --- a/compress.go +++ b/compress.go @@ -1,5 +1,15 @@ +// +build !js + package websocket +import ( + "io" + "net/http" + "sync" + + "github.com/klauspost/compress/flate" +) + // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 // @@ -37,3 +47,173 @@ const ( // important than bandwidth. CompressionDisabled ) + +func (m CompressionMode) opts() *compressionOptions { + return &compressionOptions{ + clientNoContextTakeover: m == CompressionNoContextTakeover, + serverNoContextTakeover: m == CompressionNoContextTakeover, + } +} + +type compressionOptions struct { + clientNoContextTakeover bool + serverNoContextTakeover bool +} + +func (copts *compressionOptions) setHeader(h http.Header) { + s := "permessage-deflate" + if copts.clientNoContextTakeover { + s += "; client_no_context_takeover" + } + if copts.serverNoContextTakeover { + s += "; server_no_context_takeover" + } + h.Set("Sec-WebSocket-Extensions", s) +} + +// These bytes are required to get flate.Reader to return. +// They are removed when sending to avoid the overhead as +// WebSocket framing tell's when the message has ended but then +// we need to add them back otherwise flate.Reader keeps +// trying to return more bytes. +const deflateMessageTail = "\x00\x00\xff\xff" + +type trimLastFourBytesWriter struct { + w io.Writer + tail []byte +} + +func (tw *trimLastFourBytesWriter) reset() { + if tw != nil && tw.tail != nil { + tw.tail = tw.tail[:0] + } +} + +func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { + if tw.tail == nil { + tw.tail = make([]byte, 0, 4) + } + + extra := len(tw.tail) + len(p) - 4 + + if extra <= 0 { + tw.tail = append(tw.tail, p...) + return len(p), nil + } + + // Now we need to write as many extra bytes as we can from the previous tail. + if extra > len(tw.tail) { + extra = len(tw.tail) + } + if extra > 0 { + _, err := tw.w.Write(tw.tail[:extra]) + if err != nil { + return 0, err + } + + // Shift remaining bytes in tail over. + n := copy(tw.tail, tw.tail[extra:]) + tw.tail = tw.tail[:n] + } + + // If p is less than or equal to 4 bytes, + // all of it is is part of the tail. + if len(p) <= 4 { + tw.tail = append(tw.tail, p...) + return len(p), nil + } + + // Otherwise, only the last 4 bytes are. + tw.tail = append(tw.tail, p[len(p)-4:]...) + + p = p[:len(p)-4] + n, err := tw.w.Write(p) + return n + 4, err +} + +var flateReaderPool sync.Pool + +func getFlateReader(r io.Reader, dict []byte) io.Reader { + fr, ok := flateReaderPool.Get().(io.Reader) + if !ok { + return flate.NewReaderDict(r, dict) + } + fr.(flate.Resetter).Reset(r, dict) + return fr +} + +func putFlateReader(fr io.Reader) { + flateReaderPool.Put(fr) +} + +type slidingWindow struct { + buf []byte +} + +var swPoolMu sync.RWMutex +var swPool = map[int]*sync.Pool{} + +func slidingWindowPool(n int) *sync.Pool { + swPoolMu.RLock() + p, ok := swPool[n] + swPoolMu.RUnlock() + if ok { + return p + } + + p = &sync.Pool{} + + swPoolMu.Lock() + swPool[n] = p + swPoolMu.Unlock() + + return p +} + +func (sw *slidingWindow) init(n int) { + if sw.buf != nil { + return + } + + if n == 0 { + n = 32768 + } + + p := slidingWindowPool(n) + buf, ok := p.Get().([]byte) + if ok { + sw.buf = buf[:0] + } else { + sw.buf = make([]byte, 0, n) + } +} + +func (sw *slidingWindow) close() { + if sw.buf == nil { + return + } + + swPoolMu.Lock() + swPool[cap(sw.buf)].Put(sw.buf) + swPoolMu.Unlock() + sw.buf = nil +} + +func (sw *slidingWindow) write(p []byte) { + if len(p) >= cap(sw.buf) { + sw.buf = sw.buf[:cap(sw.buf)] + p = p[len(p)-cap(sw.buf):] + copy(sw.buf, p) + return + } + + left := cap(sw.buf) - len(sw.buf) + if left < len(p) { + // We need to shift spaceNeeded bytes from the end to make room for p at the end. + spaceNeeded := len(p) - left + copy(sw.buf, sw.buf[spaceNeeded:]) + sw.buf = sw.buf[:len(sw.buf)-spaceNeeded] + } + + sw.buf = append(sw.buf, p...) +} diff --git a/compress_notjs.go b/compress_notjs.go deleted file mode 100644 index 809a272c..00000000 --- a/compress_notjs.go +++ /dev/null @@ -1,181 +0,0 @@ -// +build !js - -package websocket - -import ( - "io" - "net/http" - "sync" - - "github.com/klauspost/compress/flate" -) - -func (m CompressionMode) opts() *compressionOptions { - return &compressionOptions{ - clientNoContextTakeover: m == CompressionNoContextTakeover, - serverNoContextTakeover: m == CompressionNoContextTakeover, - } -} - -type compressionOptions struct { - clientNoContextTakeover bool - serverNoContextTakeover bool -} - -func (copts *compressionOptions) setHeader(h http.Header) { - s := "permessage-deflate" - if copts.clientNoContextTakeover { - s += "; client_no_context_takeover" - } - if copts.serverNoContextTakeover { - s += "; server_no_context_takeover" - } - h.Set("Sec-WebSocket-Extensions", s) -} - -// These bytes are required to get flate.Reader to return. -// They are removed when sending to avoid the overhead as -// WebSocket framing tell's when the message has ended but then -// we need to add them back otherwise flate.Reader keeps -// trying to return more bytes. -const deflateMessageTail = "\x00\x00\xff\xff" - -type trimLastFourBytesWriter struct { - w io.Writer - tail []byte -} - -func (tw *trimLastFourBytesWriter) reset() { - if tw != nil && tw.tail != nil { - tw.tail = tw.tail[:0] - } -} - -func (tw *trimLastFourBytesWriter) Write(p []byte) (int, error) { - if tw.tail == nil { - tw.tail = make([]byte, 0, 4) - } - - extra := len(tw.tail) + len(p) - 4 - - if extra <= 0 { - tw.tail = append(tw.tail, p...) - return len(p), nil - } - - // Now we need to write as many extra bytes as we can from the previous tail. - if extra > len(tw.tail) { - extra = len(tw.tail) - } - if extra > 0 { - _, err := tw.w.Write(tw.tail[:extra]) - if err != nil { - return 0, err - } - - // Shift remaining bytes in tail over. - n := copy(tw.tail, tw.tail[extra:]) - tw.tail = tw.tail[:n] - } - - // If p is less than or equal to 4 bytes, - // all of it is is part of the tail. - if len(p) <= 4 { - tw.tail = append(tw.tail, p...) - return len(p), nil - } - - // Otherwise, only the last 4 bytes are. - tw.tail = append(tw.tail, p[len(p)-4:]...) - - p = p[:len(p)-4] - n, err := tw.w.Write(p) - return n + 4, err -} - -var flateReaderPool sync.Pool - -func getFlateReader(r io.Reader, dict []byte) io.Reader { - fr, ok := flateReaderPool.Get().(io.Reader) - if !ok { - return flate.NewReaderDict(r, dict) - } - fr.(flate.Resetter).Reset(r, dict) - return fr -} - -func putFlateReader(fr io.Reader) { - flateReaderPool.Put(fr) -} - -type slidingWindow struct { - buf []byte -} - -var swPoolMu sync.RWMutex -var swPool = map[int]*sync.Pool{} - -func slidingWindowPool(n int) *sync.Pool { - swPoolMu.RLock() - p, ok := swPool[n] - swPoolMu.RUnlock() - if ok { - return p - } - - p = &sync.Pool{} - - swPoolMu.Lock() - swPool[n] = p - swPoolMu.Unlock() - - return p -} - -func (sw *slidingWindow) init(n int) { - if sw.buf != nil { - return - } - - if n == 0 { - n = 32768 - } - - p := slidingWindowPool(n) - buf, ok := p.Get().([]byte) - if ok { - sw.buf = buf[:0] - } else { - sw.buf = make([]byte, 0, n) - } -} - -func (sw *slidingWindow) close() { - if sw.buf == nil { - return - } - - swPoolMu.Lock() - swPool[cap(sw.buf)].Put(sw.buf) - swPoolMu.Unlock() - sw.buf = nil -} - -func (sw *slidingWindow) write(p []byte) { - if len(p) >= cap(sw.buf) { - sw.buf = sw.buf[:cap(sw.buf)] - p = p[len(p)-cap(sw.buf):] - copy(sw.buf, p) - return - } - - left := cap(sw.buf) - len(sw.buf) - if left < len(p) { - // We need to shift spaceNeeded bytes from the end to make room for p at the end. - spaceNeeded := len(p) - left - copy(sw.buf, sw.buf[spaceNeeded:]) - sw.buf = sw.buf[:len(sw.buf)-spaceNeeded] - } - - sw.buf = append(sw.buf, p...) -} diff --git a/conn.go b/conn.go index a41808be..e208d116 100644 --- a/conn.go +++ b/conn.go @@ -1,5 +1,19 @@ +// +build !js + package websocket +import ( + "bufio" + "context" + "errors" + "fmt" + "io" + "runtime" + "strconv" + "sync" + "sync/atomic" +) + // MessageType represents the type of a WebSocket message. // See https://tools.ietf.org/html/rfc6455#section-5.6 type MessageType int @@ -11,3 +25,253 @@ const ( // MessageBinary is for binary messages like protobufs. MessageBinary ) + +// Conn represents a WebSocket connection. +// All methods may be called concurrently except for Reader and Read. +// +// You must always read from the connection. Otherwise control +// frames will not be handled. See Reader and CloseRead. +// +// Be sure to call Close on the connection when you +// are finished with it to release associated resources. +// +// On any error from any method, the connection is closed +// with an appropriate reason. +type Conn struct { + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions + flateThreshold int + br *bufio.Reader + bw *bufio.Writer + + readTimeout chan context.Context + writeTimeout chan context.Context + + // Read state. + readMu *mu + readHeaderBuf [8]byte + readControlBuf [maxControlPayload]byte + msgReader *msgReader + readCloseFrameErr error + + // Write state. + msgWriterState *msgWriterState + writeFrameMu *mu + writeBuf []byte + writeHeaderBuf [8]byte + writeHeader header + + closed chan struct{} + closeMu sync.Mutex + closeErr error + wroteClose bool + + pingCounter int32 + activePingsMu sync.Mutex + activePings map[string]chan<- struct{} +} + +type connConfig struct { + subprotocol string + rwc io.ReadWriteCloser + client bool + copts *compressionOptions + flateThreshold int + + br *bufio.Reader + bw *bufio.Writer +} + +func newConn(cfg connConfig) *Conn { + c := &Conn{ + subprotocol: cfg.subprotocol, + rwc: cfg.rwc, + client: cfg.client, + copts: cfg.copts, + flateThreshold: cfg.flateThreshold, + + br: cfg.br, + bw: cfg.bw, + + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), + + closed: make(chan struct{}), + activePings: make(map[string]chan<- struct{}), + } + + c.readMu = newMu(c) + c.writeFrameMu = newMu(c) + + c.msgReader = newMsgReader(c) + + c.msgWriterState = newMsgWriterState(c) + if c.client { + c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) + } + + if c.flate() && c.flateThreshold == 0 { + c.flateThreshold = 128 + if !c.msgWriterState.flateContextTakeover() { + c.flateThreshold = 512 + } + } + + runtime.SetFinalizer(c, func(c *Conn) { + c.close(errors.New("connection garbage collected")) + }) + + go c.timeoutLoop() + + return c +} + +// Subprotocol returns the negotiated subprotocol. +// An empty string means the default protocol. +func (c *Conn) Subprotocol() string { + return c.subprotocol +} + +func (c *Conn) close(err error) { + c.closeMu.Lock() + defer c.closeMu.Unlock() + + if c.isClosed() { + return + } + c.setCloseErrLocked(err) + close(c.closed) + runtime.SetFinalizer(c, nil) + + // Have to close after c.closed is closed to ensure any goroutine that wakes up + // from the connection being closed also sees that c.closed is closed and returns + // closeErr. + c.rwc.Close() + + go func() { + c.msgWriterState.close() + + c.msgReader.close() + }() +} + +func (c *Conn) timeoutLoop() { + readCtx := context.Background() + writeCtx := context.Background() + + for { + select { + case <-c.closed: + return + + case writeCtx = <-c.writeTimeout: + case readCtx = <-c.readTimeout: + + case <-readCtx.Done(): + c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) + go c.writeError(StatusPolicyViolation, errors.New("timed out")) + case <-writeCtx.Done(): + c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) + return + } + } +} + +func (c *Conn) flate() bool { + return c.copts != nil +} + +// Ping sends a ping to the peer and waits for a pong. +// Use this to measure latency or ensure the peer is responsive. +// Ping must be called concurrently with Reader as it does +// not read from the connection but instead waits for a Reader call +// to read the pong. +// +// TCP Keepalives should suffice for most use cases. +func (c *Conn) Ping(ctx context.Context) error { + p := atomic.AddInt32(&c.pingCounter, 1) + + err := c.ping(ctx, strconv.Itoa(int(p))) + if err != nil { + return fmt.Errorf("failed to ping: %w", err) + } + return nil +} + +func (c *Conn) ping(ctx context.Context, p string) error { + pong := make(chan struct{}) + + c.activePingsMu.Lock() + c.activePings[p] = pong + c.activePingsMu.Unlock() + + defer func() { + c.activePingsMu.Lock() + delete(c.activePings, p) + c.activePingsMu.Unlock() + }() + + err := c.writeControl(ctx, opPing, []byte(p)) + if err != nil { + return err + } + + select { + case <-c.closed: + return c.closeErr + case <-ctx.Done(): + err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) + c.close(err) + return err + case <-pong: + return nil + } +} + +type mu struct { + c *Conn + ch chan struct{} +} + +func newMu(c *Conn) *mu { + return &mu{ + c: c, + ch: make(chan struct{}, 1), + } +} + +func (m *mu) forceLock() { + m.ch <- struct{}{} +} + +func (m *mu) lock(ctx context.Context) error { + select { + case <-m.c.closed: + return m.c.closeErr + case <-ctx.Done(): + err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) + m.c.close(err) + return err + case m.ch <- struct{}{}: + // To make sure the connection is certainly alive. + // As it's possible the send on m.ch was selected + // over the receive on closed. + select { + case <-m.c.closed: + // Make sure to release. + m.unlock() + return m.c.closeErr + default: + } + return nil + } +} + +func (m *mu) unlock() { + select { + case <-m.ch: + default: + } +} diff --git a/conn_notjs.go b/conn_notjs.go deleted file mode 100644 index bb2eb22f..00000000 --- a/conn_notjs.go +++ /dev/null @@ -1,265 +0,0 @@ -// +build !js - -package websocket - -import ( - "bufio" - "context" - "errors" - "fmt" - "io" - "runtime" - "strconv" - "sync" - "sync/atomic" -) - -// Conn represents a WebSocket connection. -// All methods may be called concurrently except for Reader and Read. -// -// You must always read from the connection. Otherwise control -// frames will not be handled. See Reader and CloseRead. -// -// Be sure to call Close on the connection when you -// are finished with it to release associated resources. -// -// On any error from any method, the connection is closed -// with an appropriate reason. -type Conn struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - flateThreshold int - br *bufio.Reader - bw *bufio.Writer - - readTimeout chan context.Context - writeTimeout chan context.Context - - // Read state. - readMu *mu - readHeaderBuf [8]byte - readControlBuf [maxControlPayload]byte - msgReader *msgReader - readCloseFrameErr error - - // Write state. - msgWriterState *msgWriterState - writeFrameMu *mu - writeBuf []byte - writeHeaderBuf [8]byte - writeHeader header - - closed chan struct{} - closeMu sync.Mutex - closeErr error - wroteClose bool - - pingCounter int32 - activePingsMu sync.Mutex - activePings map[string]chan<- struct{} -} - -type connConfig struct { - subprotocol string - rwc io.ReadWriteCloser - client bool - copts *compressionOptions - flateThreshold int - - br *bufio.Reader - bw *bufio.Writer -} - -func newConn(cfg connConfig) *Conn { - c := &Conn{ - subprotocol: cfg.subprotocol, - rwc: cfg.rwc, - client: cfg.client, - copts: cfg.copts, - flateThreshold: cfg.flateThreshold, - - br: cfg.br, - bw: cfg.bw, - - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), - - closed: make(chan struct{}), - activePings: make(map[string]chan<- struct{}), - } - - c.readMu = newMu(c) - c.writeFrameMu = newMu(c) - - c.msgReader = newMsgReader(c) - - c.msgWriterState = newMsgWriterState(c) - if c.client { - c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) - } - - if c.flate() && c.flateThreshold == 0 { - c.flateThreshold = 128 - if !c.msgWriterState.flateContextTakeover() { - c.flateThreshold = 512 - } - } - - runtime.SetFinalizer(c, func(c *Conn) { - c.close(errors.New("connection garbage collected")) - }) - - go c.timeoutLoop() - - return c -} - -// Subprotocol returns the negotiated subprotocol. -// An empty string means the default protocol. -func (c *Conn) Subprotocol() string { - return c.subprotocol -} - -func (c *Conn) close(err error) { - c.closeMu.Lock() - defer c.closeMu.Unlock() - - if c.isClosed() { - return - } - c.setCloseErrLocked(err) - close(c.closed) - runtime.SetFinalizer(c, nil) - - // Have to close after c.closed is closed to ensure any goroutine that wakes up - // from the connection being closed also sees that c.closed is closed and returns - // closeErr. - c.rwc.Close() - - go func() { - c.msgWriterState.close() - - c.msgReader.close() - }() -} - -func (c *Conn) timeoutLoop() { - readCtx := context.Background() - writeCtx := context.Background() - - for { - select { - case <-c.closed: - return - - case writeCtx = <-c.writeTimeout: - case readCtx = <-c.readTimeout: - - case <-readCtx.Done(): - c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - go c.writeError(StatusPolicyViolation, errors.New("timed out")) - case <-writeCtx.Done(): - c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) - return - } - } -} - -func (c *Conn) flate() bool { - return c.copts != nil -} - -// Ping sends a ping to the peer and waits for a pong. -// Use this to measure latency or ensure the peer is responsive. -// Ping must be called concurrently with Reader as it does -// not read from the connection but instead waits for a Reader call -// to read the pong. -// -// TCP Keepalives should suffice for most use cases. -func (c *Conn) Ping(ctx context.Context) error { - p := atomic.AddInt32(&c.pingCounter, 1) - - err := c.ping(ctx, strconv.Itoa(int(p))) - if err != nil { - return fmt.Errorf("failed to ping: %w", err) - } - return nil -} - -func (c *Conn) ping(ctx context.Context, p string) error { - pong := make(chan struct{}) - - c.activePingsMu.Lock() - c.activePings[p] = pong - c.activePingsMu.Unlock() - - defer func() { - c.activePingsMu.Lock() - delete(c.activePings, p) - c.activePingsMu.Unlock() - }() - - err := c.writeControl(ctx, opPing, []byte(p)) - if err != nil { - return err - } - - select { - case <-c.closed: - return c.closeErr - case <-ctx.Done(): - err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) - c.close(err) - return err - case <-pong: - return nil - } -} - -type mu struct { - c *Conn - ch chan struct{} -} - -func newMu(c *Conn) *mu { - return &mu{ - c: c, - ch: make(chan struct{}, 1), - } -} - -func (m *mu) forceLock() { - m.ch <- struct{}{} -} - -func (m *mu) lock(ctx context.Context) error { - select { - case <-m.c.closed: - return m.c.closeErr - case <-ctx.Done(): - err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) - m.c.close(err) - return err - case m.ch <- struct{}{}: - // To make sure the connection is certainly alive. - // As it's possible the send on m.ch was selected - // over the receive on closed. - select { - case <-m.c.closed: - // Make sure to release. - m.unlock() - return m.c.closeErr - default: - } - return nil - } -} - -func (m *mu) unlock() { - select { - case <-m.ch: - default: - } -} diff --git a/ws_js.go b/ws_js.go index b87e32cd..31e3c2f6 100644 --- a/ws_js.go +++ b/ws_js.go @@ -377,3 +377,137 @@ func (c *Conn) isClosed() bool { return false } } + +// AcceptOptions represents Accept's options. +type AcceptOptions struct { + Subprotocols []string + InsecureSkipVerify bool + OriginPatterns []string + CompressionMode CompressionMode + CompressionThreshold int +} + +// Accept is stubbed out for Wasm. +func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, error) { + return nil, errors.New("unimplemented") +} + +// StatusCode represents a WebSocket status code. +// https://tools.ietf.org/html/rfc6455#section-7.4 +type StatusCode int + +// https://www.iana.org/assignments/websocket/websocket.xhtml#close-code-number +// +// These are only the status codes defined by the protocol. +// +// You can define custom codes in the 3000-4999 range. +// The 3000-3999 range is reserved for use by libraries, frameworks and applications. +// The 4000-4999 range is reserved for private use. +const ( + StatusNormalClosure StatusCode = 1000 + StatusGoingAway StatusCode = 1001 + StatusProtocolError StatusCode = 1002 + StatusUnsupportedData StatusCode = 1003 + + // 1004 is reserved and so unexported. + statusReserved StatusCode = 1004 + + // StatusNoStatusRcvd cannot be sent in a close message. + // It is reserved for when a close message is received without + // a status code. + StatusNoStatusRcvd StatusCode = 1005 + + // StatusAbnormalClosure is exported for use only with Wasm. + // In non Wasm Go, the returned error will indicate whether the + // connection was closed abnormally. + StatusAbnormalClosure StatusCode = 1006 + + StatusInvalidFramePayloadData StatusCode = 1007 + StatusPolicyViolation StatusCode = 1008 + StatusMessageTooBig StatusCode = 1009 + StatusMandatoryExtension StatusCode = 1010 + StatusInternalError StatusCode = 1011 + StatusServiceRestart StatusCode = 1012 + StatusTryAgainLater StatusCode = 1013 + StatusBadGateway StatusCode = 1014 + + // StatusTLSHandshake is only exported for use with Wasm. + // In non Wasm Go, the returned error will indicate whether there was + // a TLS handshake failure. + StatusTLSHandshake StatusCode = 1015 +) + +// CloseError is returned when the connection is closed with a status and reason. +// +// Use Go 1.13's errors.As to check for this error. +// Also see the CloseStatus helper. +type CloseError struct { + Code StatusCode + Reason string +} + +func (ce CloseError) Error() string { + return fmt.Sprintf("status = %v and reason = %q", ce.Code, ce.Reason) +} + +// CloseStatus is a convenience wrapper around Go 1.13's errors.As to grab +// the status code from a CloseError. +// +// -1 will be returned if the passed error is nil or not a CloseError. +func CloseStatus(err error) StatusCode { + var ce CloseError + if errors.As(err, &ce) { + return ce.Code + } + return -1 +} + +// CompressionMode represents the modes available to the deflate extension. +// See https://tools.ietf.org/html/rfc7692 +// +// A compatibility layer is implemented for the older deflate-frame extension used +// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 +// It will work the same in every way except that we cannot signal to the peer we +// want to use no context takeover on our side, we can only signal that they should. +// It is however currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 +type CompressionMode int + +const ( + // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed + // for every message. This applies to both server and client side. + // + // This means less efficient compression as the sliding window from previous messages + // will not be used but the memory overhead will be lower if the connections + // are long lived and seldom used. + // + // The message will only be compressed if greater than 512 bytes. + CompressionNoContextTakeover CompressionMode = iota + + // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. + // This enables reusing the sliding window from previous messages. + // As most WebSocket protocols are repetitive, this can be very efficient. + // It carries an overhead of 8 kB for every connection compared to CompressionNoContextTakeover. + // + // If the peer negotiates NoContextTakeover on the client or server side, it will be + // used instead as this is required by the RFC. + CompressionContextTakeover + + // CompressionDisabled disables the deflate extension. + // + // Use this if you are using a predominantly binary protocol with very + // little duplication in between messages or CPU and memory are more + // important than bandwidth. + CompressionDisabled +) + +// MessageType represents the type of a WebSocket message. +// See https://tools.ietf.org/html/rfc6455#section-5.6 +type MessageType int + +// MessageType constants. +const ( + // MessageText is for UTF-8 encoded text messages like JSON. + MessageText MessageType = iota + 1 + // MessageBinary is for binary messages like protobufs. + MessageBinary +) From 17cf0fe86c9c23e64714986b266a15fd9a26142d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 03:12:08 -0400 Subject: [PATCH 002/152] Disable compression by default Closes #220 and #230 --- README.md | 3 +-- accept.go | 2 +- accept_test.go | 4 +++- autobahn_test.go | 4 +++- compress.go | 60 +++++++++++++++++++++++++++++------------------- conn_test.go | 4 ++-- dial.go | 2 +- go.mod | 1 - go.sum | 2 -- write.go | 35 ++++++++++++++++++---------- 10 files changed, 71 insertions(+), 46 deletions(-) diff --git a/README.md b/README.md index df20c581..8420bdbd 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ go get nhooyr.io/websocket - Minimal and idiomatic API - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) -- [Single dependency](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) +- [Zero dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) - JSON and protobuf helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb) subpackages - Zero alloc reads and writes - Concurrent writes @@ -112,7 +112,6 @@ Advantages of nhooyr.io/websocket: - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - - We use [klauspost/compress](https://github.com/klauspost/compress) for much lower memory usage ([gorilla/websocket#203](https://github.com/gorilla/websocket/issues/203)) - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) - Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) diff --git a/accept.go b/accept.go index 66379b5d..f038dec9 100644 --- a/accept.go +++ b/accept.go @@ -51,7 +51,7 @@ type AcceptOptions struct { OriginPatterns []string // CompressionMode controls the compression mode. - // Defaults to CompressionNoContextTakeover. + // Defaults to CompressionDisabled. // // See docs on CompressionMode for details. CompressionMode CompressionMode diff --git a/accept_test.go b/accept_test.go index 9b18d8e1..f7bc6693 100644 --- a/accept_test.go +++ b/accept_test.go @@ -55,7 +55,9 @@ func TestAccept(t *testing.T) { r.Header.Set("Sec-WebSocket-Key", "meow123") r.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; harharhar") - _, err := Accept(w, r, nil) + _, err := Accept(w, r, &AcceptOptions{ + CompressionMode: CompressionContextTakeover, + }) assert.Contains(t, err, `unsupported permessage-deflate parameter`) }) diff --git a/autobahn_test.go b/autobahn_test.go index e56a4912..d53159a0 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -61,7 +61,9 @@ func TestAutobahn(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute*5) defer cancel() - c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), nil) + c, _, err := websocket.Dial(ctx, fmt.Sprintf(wstestURL+"/runCase?case=%v&agent=main", i), &websocket.DialOptions{ + CompressionMode: websocket.CompressionContextTakeover, + }) assert.Success(t, err) err = wstest.EchoLoop(ctx, c) t.Logf("echoLoop: %v", err) diff --git a/compress.go b/compress.go index 63d961b4..f49d9e5d 100644 --- a/compress.go +++ b/compress.go @@ -3,49 +3,47 @@ package websocket import ( + "compress/flate" "io" "net/http" "sync" - - "github.com/klauspost/compress/flate" ) // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 -// -// A compatibility layer is implemented for the older deflate-frame extension used -// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 -// It will work the same in every way except that we cannot signal to the peer we -// want to use no context takeover on our side, we can only signal that they should. -// It is however currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 type CompressionMode int const ( - // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed - // for every message. This applies to both server and client side. + // CompressionDisabled disables the deflate extension. // - // This means less efficient compression as the sliding window from previous messages - // will not be used but the memory overhead will be lower if the connections - // are long lived and seldom used. + // Use this if you are using a predominantly binary protocol with very + // little duplication in between messages or CPU and memory are more + // important than bandwidth. // - // The message will only be compressed if greater than 512 bytes. - CompressionNoContextTakeover CompressionMode = iota + // This is the default. + CompressionDisabled CompressionMode = iota - // CompressionContextTakeover uses a flate.Reader and flate.Writer per connection. - // This enables reusing the sliding window from previous messages. + // CompressionContextTakeover uses a 32 kB sliding window and flate.Writer per connection. + // It reusing the sliding window from previous messages. // As most WebSocket protocols are repetitive, this can be very efficient. - // It carries an overhead of 8 kB for every connection compared to CompressionNoContextTakeover. + // It carries an overhead of 32 kB + 1.2 MB for every connection compared to CompressionNoContextTakeover. + // + // Sometime in the future it will carry 65 kB overhead instead once https://github.com/golang/go/issues/36919 + // is fixed. // // If the peer negotiates NoContextTakeover on the client or server side, it will be // used instead as this is required by the RFC. CompressionContextTakeover - // CompressionDisabled disables the deflate extension. + // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed + // for every message. This applies to both server and client side. // - // Use this if you are using a predominantly binary protocol with very - // little duplication in between messages or CPU and memory are more - // important than bandwidth. - CompressionDisabled + // This means less efficient compression as the sliding window from previous messages + // will not be used but the memory overhead will be lower if the connections + // are long lived and seldom used. + // + // The message will only be compressed if greater than 512 bytes. + CompressionNoContextTakeover ) func (m CompressionMode) opts() *compressionOptions { @@ -146,6 +144,22 @@ func putFlateReader(fr io.Reader) { flateReaderPool.Put(fr) } +var flateWriterPool sync.Pool + +func getFlateWriter(w io.Writer) *flate.Writer { + fw, ok := flateWriterPool.Get().(*flate.Writer) + if !ok { + fw, _ = flate.NewWriter(w, flate.BestSpeed) + return fw + } + fw.Reset(w) + return fw +} + +func putFlateWriter(w *flate.Writer) { + flateWriterPool.Put(w) +} + type slidingWindow struct { buf []byte } diff --git a/conn_test.go b/conn_test.go index c2c41292..4bab5adf 100644 --- a/conn_test.go +++ b/conn_test.go @@ -37,7 +37,7 @@ func TestConn(t *testing.T) { t.Parallel() compressionMode := func() websocket.CompressionMode { - return websocket.CompressionMode(xrand.Int(int(websocket.CompressionDisabled) + 1)) + return websocket.CompressionMode(xrand.Int(int(websocket.CompressionContextTakeover) + 1)) } for i := 0; i < 5; i++ { @@ -389,7 +389,7 @@ func BenchmarkConn(b *testing.B) { mode: websocket.CompressionDisabled, }, { - name: "compress", + name: "compressContextTakeover", mode: websocket.CompressionContextTakeover, }, { diff --git a/dial.go b/dial.go index 2b25e351..9ec90444 100644 --- a/dial.go +++ b/dial.go @@ -35,7 +35,7 @@ type DialOptions struct { Subprotocols []string // CompressionMode controls the compression mode. - // Defaults to CompressionNoContextTakeover. + // Defaults to CompressionDisabled. // // See docs on CompressionMode for details. CompressionMode CompressionMode diff --git a/go.mod b/go.mod index c5f1a20f..d4bca923 100644 --- a/go.mod +++ b/go.mod @@ -10,6 +10,5 @@ require ( github.com/golang/protobuf v1.3.5 github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 - github.com/klauspost/compress v1.10.3 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 ) diff --git a/go.sum b/go.sum index 155c3013..1344e958 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvK github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= diff --git a/write.go b/write.go index 2210cf81..b1c57c1b 100644 --- a/write.go +++ b/write.go @@ -12,7 +12,7 @@ import ( "io" "time" - "github.com/klauspost/compress/flate" + "compress/flate" "nhooyr.io/websocket/internal/errd" ) @@ -76,8 +76,8 @@ type msgWriterState struct { opcode opcode flate bool - trimWriter *trimLastFourBytesWriter - dict slidingWindow + trimWriter *trimLastFourBytesWriter + flateWriter *flate.Writer } func newMsgWriterState(c *Conn) *msgWriterState { @@ -96,7 +96,9 @@ func (mw *msgWriterState) ensureFlate() { } } - mw.dict.init(8192) + if mw.flateWriter == nil { + mw.flateWriter = getFlateWriter(mw.trimWriter) + } mw.flate = true } @@ -153,6 +155,13 @@ func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { return nil } +func (mw *msgWriterState) putFlateWriter() { + if mw.flateWriter != nil { + putFlateWriter(mw.flateWriter) + mw.flateWriter = nil + } +} + // Write writes the given bytes to the WebSocket connection. func (mw *msgWriterState) Write(p []byte) (_ int, err error) { err = mw.writeMu.lock(mw.ctx) @@ -177,12 +186,7 @@ func (mw *msgWriterState) Write(p []byte) (_ int, err error) { } if mw.flate { - err = flate.StatelessDeflate(mw.trimWriter, p, false, mw.dict.buf) - if err != nil { - return 0, err - } - mw.dict.write(p) - return len(p), nil + return mw.flateWriter.Write(p) } return mw.write(p) @@ -207,13 +211,20 @@ func (mw *msgWriterState) Close() (err error) { } defer mw.writeMu.unlock() + if mw.flate { + err = mw.flateWriter.Flush() + if err != nil { + return fmt.Errorf("failed to flush flate: %w", err) + } + } + _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return fmt.Errorf("failed to write fin frame: %w", err) } if mw.flate && !mw.flateContextTakeover() { - mw.dict.close() + mw.putFlateWriter() } mw.mu.unlock() return nil @@ -226,7 +237,7 @@ func (mw *msgWriterState) close() { } mw.writeMu.forceLock() - mw.dict.close() + mw.putFlateWriter() } func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error { From de8e29bdb753bc55c8f742c664adb44833afbc50 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 04:25:52 -0400 Subject: [PATCH 003/152] Fix tests taking too long and switch to t.Cleanup --- autobahn_test.go | 7 ++++++- conn_test.go | 47 +++++++++++++---------------------------------- 2 files changed, 19 insertions(+), 35 deletions(-) diff --git a/autobahn_test.go b/autobahn_test.go index d53159a0..5bf0062c 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -28,7 +28,6 @@ var excludedAutobahnCases = []string{ // We skip the tests related to requestMaxWindowBits as that is unimplemented due // to limitations in compress/flate. See https://github.com/golang/go/issues/3155 - // Same with klauspost/compress which doesn't allow adjusting the sliding window size. "13.3.*", "13.4.*", "13.5.*", "13.6.*", } @@ -41,6 +40,12 @@ func TestAutobahn(t *testing.T) { t.SkipNow() } + if os.Getenv("AUTOBAHN_FAST") != "" { + excludedAutobahnCases = append(excludedAutobahnCases, + "9.*", "13.*", "12.*", + ) + } + ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) defer cancel() diff --git a/conn_test.go b/conn_test.go index 4bab5adf..9c85459e 100644 --- a/conn_test.go +++ b/conn_test.go @@ -49,7 +49,6 @@ func TestConn(t *testing.T) { CompressionMode: compressionMode(), CompressionThreshold: xrand.Int(9999), }) - defer tt.cleanup() tt.goEchoLoop(c2) @@ -67,8 +66,9 @@ func TestConn(t *testing.T) { }) t.Run("badClose", func(t *testing.T) { - tt, c1, _ := newConnTest(t, nil, nil) - defer tt.cleanup() + tt, c1, c2 := newConnTest(t, nil, nil) + + c2.CloseRead(tt.ctx) err := c1.Close(-1, "") assert.Contains(t, err, "failed to marshal close frame: status code StatusCode(-1) cannot be set") @@ -76,7 +76,6 @@ func TestConn(t *testing.T) { t.Run("ping", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() c1.CloseRead(tt.ctx) c2.CloseRead(tt.ctx) @@ -92,7 +91,6 @@ func TestConn(t *testing.T) { t.Run("badPing", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() c2.CloseRead(tt.ctx) @@ -105,7 +103,6 @@ func TestConn(t *testing.T) { t.Run("concurrentWrite", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() tt.goDiscardLoop(c2) @@ -138,7 +135,6 @@ func TestConn(t *testing.T) { t.Run("concurrentWriteError", func(t *testing.T) { tt, c1, _ := newConnTest(t, nil, nil) - defer tt.cleanup() _, err := c1.Writer(tt.ctx, websocket.MessageText) assert.Success(t, err) @@ -152,7 +148,6 @@ func TestConn(t *testing.T) { t.Run("netConn", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) @@ -192,17 +187,14 @@ func TestConn(t *testing.T) { t.Run("netConn/BadMsg", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageText) + c2.CloseRead(tt.ctx) errs := xsync.Go(func() error { _, err := n2.Write([]byte("hello")) - if err != nil { - return err - } - return nil + return err }) _, err := ioutil.ReadAll(n1) @@ -218,7 +210,6 @@ func TestConn(t *testing.T) { t.Run("wsjson", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() tt.goEchoLoop(c2) @@ -248,7 +239,6 @@ func TestConn(t *testing.T) { t.Run("wspb", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) - defer tt.cleanup() tt.goEchoLoop(c2) @@ -305,8 +295,6 @@ func assertCloseStatus(exp websocket.StatusCode, err error) error { type connTest struct { t testing.TB ctx context.Context - - doneFuncs []func() } func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) (tt *connTest, c1, c2 *websocket.Conn) { @@ -317,30 +305,22 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) tt = &connTest{t: t, ctx: ctx} - tt.appendDone(cancel) + t.Cleanup(cancel) c1, c2 = wstest.Pipe(dialOpts, acceptOpts) if xrand.Bool() { c1, c2 = c2, c1 } - tt.appendDone(func() { - c2.Close(websocket.StatusInternalError, "") - c1.Close(websocket.StatusInternalError, "") + t.Cleanup(func() { + // We don't actually care whether this succeeds so we just run it in a separate goroutine to avoid + // blocking the test shutting down. + go c2.Close(websocket.StatusInternalError, "") + go c1.Close(websocket.StatusInternalError, "") }) return tt, c1, c2 } -func (tt *connTest) appendDone(f func()) { - tt.doneFuncs = append(tt.doneFuncs, f) -} - -func (tt *connTest) cleanup() { - for i := len(tt.doneFuncs) - 1; i >= 0; i-- { - tt.doneFuncs[i]() - } -} - func (tt *connTest) goEchoLoop(c *websocket.Conn) { ctx, cancel := context.WithCancel(tt.ctx) @@ -348,7 +328,7 @@ func (tt *connTest) goEchoLoop(c *websocket.Conn) { err := wstest.EchoLoop(ctx, c) return assertCloseStatus(websocket.StatusNormalClosure, err) }) - tt.appendDone(func() { + tt.t.Cleanup(func() { cancel() err := <-echoLoopErr if err != nil { @@ -370,7 +350,7 @@ func (tt *connTest) goDiscardLoop(c *websocket.Conn) { } } }) - tt.appendDone(func() { + tt.t.Cleanup(func() { cancel() err := <-discardLoopErr if err != nil { @@ -404,7 +384,6 @@ func BenchmarkConn(b *testing.B) { }, &websocket.AcceptOptions{ CompressionMode: bc.mode, }) - defer bb.cleanup() bb.goEchoLoop(c2) From 169521697c04f5b5a06b3da51bf4cad56884d2b6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 14:09:47 -0400 Subject: [PATCH 004/152] Add ping example Closes #227 --- autobahn_test.go | 5 +++-- example_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/autobahn_test.go b/autobahn_test.go index 5bf0062c..7c735a38 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -36,11 +36,12 @@ var autobahnCases = []string{"*"} func TestAutobahn(t *testing.T) { t.Parallel() - if os.Getenv("AUTOBAHN_TEST") == "" { + if os.Getenv("AUTOBAHN") == "" { t.SkipNow() } - if os.Getenv("AUTOBAHN_FAST") != "" { + if os.Getenv("AUTOBAHN") == "fast" { + // These are the slow tests. excludedAutobahnCases = append(excludedAutobahnCases, "9.*", "13.*", "12.*", ) diff --git a/example_test.go b/example_test.go index 632c4d6e..d44bd537 100644 --- a/example_test.go +++ b/example_test.go @@ -135,6 +135,31 @@ func Example_crossOrigin() { log.Fatal(err) } +func ExampleConn_Ping() { + // Dials a server and pings it 5 times. + + ctx, cancel := context.WithTimeout(context.Background(), time.Minute) + defer cancel() + + c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil) + if err != nil { + log.Fatal(err) + } + defer c.Close(websocket.StatusInternalError, "the sky is falling") + + // Required to read the Pongs from the server. + ctx = c.CloseRead(ctx) + + for i := 0; i < 5; i++ { + err = c.Ping(ctx) + if err != nil { + log.Fatal(err) + } + } + + c.Close(websocket.StatusNormalClosure, "") +} + // This example demonstrates how to create a WebSocket server // that gracefully exits when sent a signal. // From 0a61ffe87a498f8ff9fef8020bee799cfa4f927f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Mon, 18 May 2020 19:09:38 -0400 Subject: [PATCH 005/152] Make SetDeadline on NetConn not always close Conn NetConn has to close the connection to interrupt in progress reads and writes. However, it can block reads and writes that occur after the deadline instead of closing the connection. Closes #228 --- conn.go | 9 ++++ netconn.go | 128 +++++++++++++++++++++++++++++++++++------------------ ws_js.go | 32 ++++++++++++++ 3 files changed, 126 insertions(+), 43 deletions(-) diff --git a/conn.go b/conn.go index e208d116..1a57c656 100644 --- a/conn.go +++ b/conn.go @@ -246,6 +246,15 @@ func (m *mu) forceLock() { m.ch <- struct{}{} } +func (m *mu) tryLock() bool { + select { + case m.ch <- struct{}{}: + return true + default: + return false + } +} + func (m *mu) lock(ctx context.Context) error { select { case <-m.c.closed: diff --git a/netconn.go b/netconn.go index 64aadf0b..ae04b20a 100644 --- a/netconn.go +++ b/netconn.go @@ -6,7 +6,7 @@ import ( "io" "math" "net" - "sync" + "sync/atomic" "time" ) @@ -28,9 +28,10 @@ import ( // // Close will close the *websocket.Conn with StatusNormalClosure. // -// When a deadline is hit, the connection will be closed. This is -// different from most net.Conn implementations where only the -// reading/writing goroutines are interrupted but the connection is kept alive. +// When a deadline is hit and there is an active read or write goroutine, the +// connection will be closed. This is different from most net.Conn implementations +// where only the reading/writing goroutines are interrupted but the connection +// is kept alive. // // The Addr methods will return a mock net.Addr that returns "websocket" for Network // and "websocket/unknown-addr" for String. @@ -41,17 +42,43 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { nc := &netConn{ c: c, msgType: msgType, + readMu: newMu(c), + writeMu: newMu(c), } - var cancel context.CancelFunc - nc.writeContext, cancel = context.WithCancel(ctx) - nc.writeTimer = time.AfterFunc(math.MaxInt64, cancel) + var writeCancel context.CancelFunc + nc.writeCtx, writeCancel = context.WithCancel(ctx) + var readCancel context.CancelFunc + nc.readCtx, readCancel = context.WithCancel(ctx) + + nc.writeTimer = time.AfterFunc(math.MaxInt64, func() { + if !nc.writeMu.tryLock() { + // If the lock cannot be acquired, then there is an + // active write goroutine and so we should cancel the context. + writeCancel() + return + } + defer nc.writeMu.unlock() + + // Prevents future writes from writing until the deadline is reset. + atomic.StoreInt64(&nc.writeExpired, 1) + }) if !nc.writeTimer.Stop() { <-nc.writeTimer.C } - nc.readContext, cancel = context.WithCancel(ctx) - nc.readTimer = time.AfterFunc(math.MaxInt64, cancel) + nc.readTimer = time.AfterFunc(math.MaxInt64, func() { + if !nc.readMu.tryLock() { + // If the lock cannot be acquired, then there is an + // active read goroutine and so we should cancel the context. + readCancel() + return + } + defer nc.readMu.unlock() + + // Prevents future reads from reading until the deadline is reset. + atomic.StoreInt64(&nc.readExpired, 1) + }) if !nc.readTimer.Stop() { <-nc.readTimer.C } @@ -64,59 +91,72 @@ type netConn struct { msgType MessageType writeTimer *time.Timer - writeContext context.Context + writeMu *mu + writeExpired int64 + writeCtx context.Context readTimer *time.Timer - readContext context.Context - - readMu sync.Mutex - eofed bool - reader io.Reader + readMu *mu + readExpired int64 + readCtx context.Context + readEOFed bool + reader io.Reader } var _ net.Conn = &netConn{} -func (c *netConn) Close() error { - return c.c.Close(StatusNormalClosure, "") +func (nc *netConn) Close() error { + return nc.c.Close(StatusNormalClosure, "") } -func (c *netConn) Write(p []byte) (int, error) { - err := c.c.Write(c.writeContext, c.msgType, p) +func (nc *netConn) Write(p []byte) (int, error) { + nc.writeMu.forceLock() + defer nc.writeMu.unlock() + + if atomic.LoadInt64(&nc.writeExpired) == 1 { + return 0, fmt.Errorf("failed to write: %w", context.DeadlineExceeded) + } + + err := nc.c.Write(nc.writeCtx, nc.msgType, p) if err != nil { return 0, err } return len(p), nil } -func (c *netConn) Read(p []byte) (int, error) { - c.readMu.Lock() - defer c.readMu.Unlock() +func (nc *netConn) Read(p []byte) (int, error) { + nc.readMu.forceLock() + defer nc.readMu.unlock() + + if atomic.LoadInt64(&nc.readExpired) == 1 { + return 0, fmt.Errorf("failed to read: %w", context.DeadlineExceeded) + } - if c.eofed { + if nc.readEOFed { return 0, io.EOF } - if c.reader == nil { - typ, r, err := c.c.Reader(c.readContext) + if nc.reader == nil { + typ, r, err := nc.c.Reader(nc.readCtx) if err != nil { switch CloseStatus(err) { case StatusNormalClosure, StatusGoingAway: - c.eofed = true + nc.readEOFed = true return 0, io.EOF } return 0, err } - if typ != c.msgType { - err := fmt.Errorf("unexpected frame type read (expected %v): %v", c.msgType, typ) - c.c.Close(StatusUnsupportedData, err.Error()) + if typ != nc.msgType { + err := fmt.Errorf("unexpected frame type read (expected %v): %v", nc.msgType, typ) + nc.c.Close(StatusUnsupportedData, err.Error()) return 0, err } - c.reader = r + nc.reader = r } - n, err := c.reader.Read(p) + n, err := nc.reader.Read(p) if err == io.EOF { - c.reader = nil + nc.reader = nil err = nil } return n, err @@ -133,34 +173,36 @@ func (a websocketAddr) String() string { return "websocket/unknown-addr" } -func (c *netConn) RemoteAddr() net.Addr { +func (nc *netConn) RemoteAddr() net.Addr { return websocketAddr{} } -func (c *netConn) LocalAddr() net.Addr { +func (nc *netConn) LocalAddr() net.Addr { return websocketAddr{} } -func (c *netConn) SetDeadline(t time.Time) error { - c.SetWriteDeadline(t) - c.SetReadDeadline(t) +func (nc *netConn) SetDeadline(t time.Time) error { + nc.SetWriteDeadline(t) + nc.SetReadDeadline(t) return nil } -func (c *netConn) SetWriteDeadline(t time.Time) error { +func (nc *netConn) SetWriteDeadline(t time.Time) error { + atomic.StoreInt64(&nc.writeExpired, 0) if t.IsZero() { - c.writeTimer.Stop() + nc.writeTimer.Stop() } else { - c.writeTimer.Reset(t.Sub(time.Now())) + nc.writeTimer.Reset(t.Sub(time.Now())) } return nil } -func (c *netConn) SetReadDeadline(t time.Time) error { +func (nc *netConn) SetReadDeadline(t time.Time) error { + atomic.StoreInt64(&nc.readExpired, 0) if t.IsZero() { - c.readTimer.Stop() + nc.readTimer.Stop() } else { - c.readTimer.Reset(t.Sub(time.Now())) + nc.readTimer.Reset(t.Sub(time.Now())) } return nil } diff --git a/ws_js.go b/ws_js.go index 31e3c2f6..d1361328 100644 --- a/ws_js.go +++ b/ws_js.go @@ -511,3 +511,35 @@ const ( // MessageBinary is for binary messages like protobufs. MessageBinary ) + +type mu struct { + c *Conn + ch chan struct{} +} + +func newMu(c *Conn) *mu { + return &mu{ + c: c, + ch: make(chan struct{}, 1), + } +} + +func (m *mu) forceLock() { + m.ch <- struct{}{} +} + +func (m *mu) tryLock() bool { + select { + case m.ch <- struct{}{}: + return true + default: + return false + } +} + +func (m *mu) unlock() { + select { + case <-m.ch: + default: + } +} From 15a152334e5aacc0158b541e135fe9f0834696dd Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 4 Jul 2020 19:21:25 -0400 Subject: [PATCH 006/152] ci/fmt.sh: Cleanup --- ci/fmt.sh | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index e6a2d689..b34f1438 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -21,11 +21,11 @@ main() { stringer -type=opcode,MessageType,StatusCode -output=stringer.go if [[ ${CI-} ]]; then - ensure_fmt + assert_no_changes fi } -ensure_fmt() { +assert_no_changes() { if [[ $(git ls-files --other --modified --exclude-standard) ]]; then git -c color.ui=always --no-pager diff echo From 493ebbe9373d536b64e122b54dc2f56ad7b79b12 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 5 Jul 2020 17:01:55 -0400 Subject: [PATCH 007/152] netconn.go: Prevent timer leakage (#255) Closes #243 --- netconn.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netconn.go b/netconn.go index ae04b20a..1664e29b 100644 --- a/netconn.go +++ b/netconn.go @@ -106,6 +106,8 @@ type netConn struct { var _ net.Conn = &netConn{} func (nc *netConn) Close() error { + nc.writeTimer.Stop() + nc.readTimer.Stop() return nc.c.Close(StatusNormalClosure, "") } From 897a573291bed65c3528f779406add491d096a7f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 5 Jul 2020 17:02:20 -0400 Subject: [PATCH 008/152] write.go: Fix deadlock in writeFrame (#253) Closes #248 Luckily, due to the 5s timeout on the close handshake, this would have had very minimal effects on anyone in production. --- write.go | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/write.go b/write.go index b1c57c1b..58bfdf9a 100644 --- a/write.go +++ b/write.go @@ -257,7 +257,6 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if err != nil { return 0, err } - defer c.writeFrameMu.unlock() // If the state says a close has already been written, we wait until // the connection is closed and return that error. @@ -268,6 +267,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco wroteClose := c.wroteClose c.closeMu.Unlock() if wroteClose && opcode != opClose { + c.writeFrameMu.unlock() select { case <-ctx.Done(): return 0, ctx.Err() @@ -275,6 +275,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco return 0, c.closeErr } } + defer c.writeFrameMu.unlock() select { case <-c.closed: From fdc407913d18e6fff8feacf9bc50f8545c234d9d Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Wed, 23 Sep 2020 23:38:22 -0700 Subject: [PATCH 009/152] Clone options (#259) See: https://staticcheck.io/docs/checks#SA4001 --- accept.go | 14 +++++++++----- dial.go | 26 +++++++++++++++----------- 2 files changed, 24 insertions(+), 16 deletions(-) diff --git a/accept.go b/accept.go index f038dec9..428abba4 100644 --- a/accept.go +++ b/accept.go @@ -63,6 +63,14 @@ type AcceptOptions struct { CompressionThreshold int } +func (opts *AcceptOptions) cloneWithDefaults() *AcceptOptions { + var o AcceptOptions + if opts != nil { + o = *opts + } + return &o +} + // Accept accepts a WebSocket handshake from a client and upgrades the // the connection to a WebSocket. // @@ -77,17 +85,13 @@ func Accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (*Conn, func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Conn, err error) { defer errd.Wrap(&err, "failed to accept WebSocket connection") - if opts == nil { - opts = &AcceptOptions{} - } - opts = &*opts - errCode, err := verifyClientRequest(w, r) if err != nil { http.Error(w, err.Error(), errCode) return nil, err } + opts = opts.cloneWithDefaults() if !opts.InsecureSkipVerify { err = authenticateOrigin(r, opts.OriginPatterns) if err != nil { diff --git a/dial.go b/dial.go index 9ec90444..d5d2266e 100644 --- a/dial.go +++ b/dial.go @@ -47,6 +47,20 @@ type DialOptions struct { CompressionThreshold int } +func (opts *DialOptions) cloneWithDefaults() *DialOptions { + var o DialOptions + if opts != nil { + o = *opts + } + if o.HTTPClient == nil { + o.HTTPClient = http.DefaultClient + } + if o.HTTPHeader == nil { + o.HTTPHeader = http.Header{} + } + return &o +} + // Dial performs a WebSocket handshake on url. // // The response is the WebSocket handshake response from the server. @@ -67,17 +81,7 @@ func Dial(ctx context.Context, u string, opts *DialOptions) (*Conn, *http.Respon func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) (_ *Conn, _ *http.Response, err error) { defer errd.Wrap(&err, "failed to WebSocket dial") - if opts == nil { - opts = &DialOptions{} - } - - opts = &*opts - if opts.HTTPClient == nil { - opts.HTTPClient = http.DefaultClient - } - if opts.HTTPHeader == nil { - opts.HTTPHeader = http.Header{} - } + opts = opts.cloneWithDefaults() secWebSocketKey, err := secWebSocketKey(rand) if err != nil { From fe1020d9fa5d2a910ac04df301eec6fa1e9aab58 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 07:33:26 -0500 Subject: [PATCH 010/152] Fix incorrect &*var clones Thank you @icholy for identifying these in https://github.com/nhooyr/websocket/pull/259#issuecomment-702279421 --- dial.go | 3 ++- internal/test/wstest/pipe.go | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/dial.go b/dial.go index 7c959bff..a79b55e6 100644 --- a/dial.go +++ b/dial.go @@ -250,7 +250,8 @@ func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compress return nil, fmt.Errorf("WebSocket protcol violation: unsupported extensions from server: %+v", exts[1:]) } - copts = &*copts + _copts := *copts + copts = &_copts for _, p := range ext.params { switch p { diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index 1534f316..f3d4c517 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -24,7 +24,8 @@ func Pipe(dialOpts *websocket.DialOptions, acceptOpts *websocket.AcceptOptions) if dialOpts == nil { dialOpts = &websocket.DialOptions{} } - dialOpts = &*dialOpts + _dialOpts := *dialOpts + dialOpts = &_dialOpts dialOpts.HTTPClient = &http.Client{ Transport: tt, } From e4fee52874b402afcb4cc7aa5cebddc393618800 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 07:30:29 -0500 Subject: [PATCH 011/152] ci/test.sh: Work with BSD sed --- ci/test.sh | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/ci/test.sh b/ci/test.sh index 95ef7101..bd68b80e 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -5,9 +5,9 @@ main() { cd "$(dirname "$0")/.." go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... "$@" ./... - sed -i '/stringer\.go/d' ci/out/coverage.prof - sed -i '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof - sed -i '/examples/d' ci/out/coverage.prof + sed -i.bak '/stringer\.go/d' ci/out/coverage.prof + sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof + sed -i.bak '/examples/d' ci/out/coverage.prof # Last line is the total coverage. go tool cover -func ci/out/coverage.prof | tail -n1 From 3b20a49a2c6fc9aa28be7d5296afe2079ff0e537 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:01:54 -0500 Subject: [PATCH 012/152] Add back documentation on separate idle and read timeout Closes #87 --- read.go | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/read.go b/read.go index afd08cc7..87151dcb 100644 --- a/read.go +++ b/read.go @@ -26,6 +26,11 @@ import ( // Call CloseRead if you do not expect any data messages from the peer. // // Only one Reader may be open at a time. +// +// If you need a separate timeout on the Reader call and the Read itself, +// use time.AfterFunc to cancel the context passed in. +// See https://github.com/nhooyr/websocket/issues/87#issue-451703332 +// Most users should not need this. func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { return c.reader(ctx) } From 29f527b17fdcba1ecd29b83f55dc7ff1114d2102 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:03:11 -0500 Subject: [PATCH 013/152] Remove ExampleGrace for now to avoid confusion --- example_test.go | 52 ------------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/example_test.go b/example_test.go index d44bd537..2e55eb96 100644 --- a/example_test.go +++ b/example_test.go @@ -160,58 +160,6 @@ func ExampleConn_Ping() { c.Close(websocket.StatusNormalClosure, "") } -// This example demonstrates how to create a WebSocket server -// that gracefully exits when sent a signal. -// -// It starts a WebSocket server that keeps every connection open -// for 10 seconds. -// If you CTRL+C while a connection is open, it will wait at most 30s -// for all connections to terminate before shutting down. -// func ExampleGrace() { -// fn := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { -// c, err := websocket.Accept(w, r, nil) -// if err != nil { -// log.Println(err) -// return -// } -// defer c.Close(websocket.StatusInternalError, "the sky is falling") -// -// ctx := c.CloseRead(r.Context()) -// select { -// case <-ctx.Done(): -// case <-time.After(time.Second * 10): -// } -// -// c.Close(websocket.StatusNormalClosure, "") -// }) -// -// var g websocket.Grace -// s := &http.Server{ -// Handler: g.Handler(fn), -// ReadTimeout: time.Second * 15, -// WriteTimeout: time.Second * 15, -// } -// -// errc := make(chan error, 1) -// go func() { -// errc <- s.ListenAndServe() -// }() -// -// sigs := make(chan os.Signal, 1) -// signal.Notify(sigs, os.Interrupt) -// select { -// case err := <-errc: -// log.Printf("failed to listen and serve: %v", err) -// case sig := <-sigs: -// log.Printf("terminating: %v", sig) -// } -// -// ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) -// defer cancel() -// s.Shutdown(ctx) -// g.Shutdown(ctx) -// } - // This example demonstrates full stack chat with an automated test. func Example_fullStackChat() { // https://github.com/nhooyr/websocket/tree/master/examples/chat From 085d46c46dde55c3ffe776ebb953ba5e93559c01 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:09:58 -0500 Subject: [PATCH 014/152] Document context expirations wart Closes #242 --- conn.go | 3 +++ 1 file changed, 3 insertions(+) diff --git a/conn.go b/conn.go index 1a57c656..beb26cec 100644 --- a/conn.go +++ b/conn.go @@ -37,6 +37,9 @@ const ( // // On any error from any method, the connection is closed // with an appropriate reason. +// +// This applies to context expirations as well unfortunately. +// See https://github.com/nhooyr/websocket/issues/242#issuecomment-633182220 type Conn struct { subprotocol string rwc io.ReadWriteCloser From ea87744105d79f972e58404bb46791b97fc3f314 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 4 Jul 2020 19:34:24 -0400 Subject: [PATCH 015/152] netconn: Disable read limit on WebSocket Closes #245 --- netconn.go | 4 ++++ read.go | 21 +++++++++++++++++---- 2 files changed, 21 insertions(+), 4 deletions(-) diff --git a/netconn.go b/netconn.go index 1664e29b..c6f8dc13 100644 --- a/netconn.go +++ b/netconn.go @@ -38,7 +38,11 @@ import ( // // A received StatusNormalClosure or StatusGoingAway close frame will be translated to // io.EOF when reading. +// +// Furthermore, the ReadLimit is set to -1 to disable it. func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { + c.SetReadLimit(-1) + nc := &netConn{ c: c, msgType: msgType, diff --git a/read.go b/read.go index 87151dcb..c4234f20 100644 --- a/read.go +++ b/read.go @@ -74,10 +74,16 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { // By default, the connection has a message read limit of 32768 bytes. // // When the limit is hit, the connection will be closed with StatusMessageTooBig. +// +// Set to -1 to disable. func (c *Conn) SetReadLimit(n int64) { - // We add read one more byte than the limit in case - // there is a fin frame that needs to be read. - c.msgReader.limitReader.limit.Store(n + 1) + if n >= 0 { + // We read one more byte than the limit in case + // there is a fin frame that needs to be read. + n++ + } + + c.msgReader.limitReader.limit.Store(n) } const defaultReadLimit = 32768 @@ -455,7 +461,11 @@ func (lr *limitReader) reset(r io.Reader) { } func (lr *limitReader) Read(p []byte) (int, error) { - if lr.n <= 0 { + if lr.n < 0 { + return lr.r.Read(p) + } + + if lr.n == 0 { err := fmt.Errorf("read limited at %v bytes", lr.limit.Load()) lr.c.writeError(StatusMessageTooBig, err) return 0, err @@ -466,6 +476,9 @@ func (lr *limitReader) Read(p []byte) (int, error) { } n, err := lr.r.Read(p) lr.n -= int64(n) + if lr.n < 0 { + lr.n = 0 + } return n, err } From 11af7f8bc0b3c3125a18ff0a0008b95e8a1e50e1 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:14:49 -0500 Subject: [PATCH 016/152] netconn: Add test for disabled read limit --- conn_test.go | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/conn_test.go b/conn_test.go index 9c85459e..3ca810c5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -208,6 +208,37 @@ func TestConn(t *testing.T) { } }) + t.Run("netConn/readLimit", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, nil, nil) + + n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) + n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) + + s := strings.Repeat("papa", 1 << 20) + errs := xsync.Go(func() error { + _, err := n2.Write([]byte(s)) + if err != nil { + return err + } + return n2.Close() + }) + + b, err := ioutil.ReadAll(n1) + assert.Success(t, err) + + _, err = n1.Read(nil) + assert.Equal(t, "read error", err, io.EOF) + + select { + case err := <-errs: + assert.Success(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + assert.Equal(t, "read msg", s, string(b)) + }) + t.Run("wsjson", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) From 482f5845b6e345293575c727253dc7b46bce4905 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:22:31 -0500 Subject: [PATCH 017/152] netconn.go: Cleanup contexts on close Updates #255 --- netconn.go | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/netconn.go b/netconn.go index c6f8dc13..aea1a02d 100644 --- a/netconn.go +++ b/netconn.go @@ -50,16 +50,14 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { writeMu: newMu(c), } - var writeCancel context.CancelFunc - nc.writeCtx, writeCancel = context.WithCancel(ctx) - var readCancel context.CancelFunc - nc.readCtx, readCancel = context.WithCancel(ctx) + nc.writeCtx, nc.writeCancel = context.WithCancel(ctx) + nc.readCtx, nc.readCancel = context.WithCancel(ctx) nc.writeTimer = time.AfterFunc(math.MaxInt64, func() { if !nc.writeMu.tryLock() { // If the lock cannot be acquired, then there is an // active write goroutine and so we should cancel the context. - writeCancel() + nc.writeCancel() return } defer nc.writeMu.unlock() @@ -75,7 +73,7 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { if !nc.readMu.tryLock() { // If the lock cannot be acquired, then there is an // active read goroutine and so we should cancel the context. - readCancel() + nc.readCancel() return } defer nc.readMu.unlock() @@ -98,11 +96,13 @@ type netConn struct { writeMu *mu writeExpired int64 writeCtx context.Context + writeCancel context.CancelFunc readTimer *time.Timer readMu *mu readExpired int64 readCtx context.Context + readCancel context.CancelFunc readEOFed bool reader io.Reader } @@ -111,7 +111,9 @@ var _ net.Conn = &netConn{} func (nc *netConn) Close() error { nc.writeTimer.Stop() + nc.writeCancel() nc.readTimer.Stop() + nc.readCancel() return nc.c.Close(StatusNormalClosure, "") } From 29251d03c03fc0f6cad649a50c31c608db8999ae Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:35:37 -0500 Subject: [PATCH 018/152] accept.go: Improve unauthorized origin error message Closes #247 --- accept.go | 5 ++++- accept_test.go | 18 +++++++++++++++++- 2 files changed, 21 insertions(+), 2 deletions(-) diff --git a/accept.go b/accept.go index 6e1f494e..542b61e8 100644 --- a/accept.go +++ b/accept.go @@ -215,7 +215,10 @@ func authenticateOrigin(r *http.Request, originHosts []string) error { return nil } } - return fmt.Errorf("request Origin %q is not authorized for Host %q", origin, r.Host) + if u.Host == "" { + return fmt.Errorf("request Origin %q is not a valid URL with a host", origin) + } + return fmt.Errorf("request Origin %q is not authorized for Host %q", u.Host, r.Host) } func match(pattern, s string) (bool, error) { diff --git a/accept_test.go b/accept_test.go index d19f54e1..67ece253 100644 --- a/accept_test.go +++ b/accept_test.go @@ -39,7 +39,23 @@ func TestAccept(t *testing.T) { r.Header.Set("Origin", "harhar.com") _, err := Accept(w, r, nil) - assert.Contains(t, err, `request Origin "harhar.com" is not authorized for Host`) + assert.Contains(t, err, `request Origin "harhar.com" is not a valid URL with a host`) + }) + + // #247 + t.Run("unauthorizedOriginErrorMessage", func(t *testing.T) { + t.Parallel() + + w := httptest.NewRecorder() + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Origin", "https://harhar.com") + + _, err := Accept(w, r, nil) + assert.Contains(t, err, `request Origin "harhar.com" is not authorized for Host "example.com"`) }) t.Run("badCompression", func(t *testing.T) { From 7c0c0470590124d0ddd3f334765402c0605549f3 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:36:33 -0500 Subject: [PATCH 019/152] Fix formatting --- conn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index 3ca810c5..0fbd1740 100644 --- a/conn_test.go +++ b/conn_test.go @@ -214,7 +214,7 @@ func TestConn(t *testing.T) { n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) - s := strings.Repeat("papa", 1 << 20) + s := strings.Repeat("papa", 1<<20) errs := xsync.Go(func() error { _, err := n2.Write([]byte(s)) if err != nil { From 6840778f54a29b77a58c43e7f5c58c4609ab10f2 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:39:23 -0500 Subject: [PATCH 020/152] README.md: Update coverage --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8420bdbd..0ae739a0 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-88%25-success)](https://nhooyrio-websocket-coverage.netlify.app) +[![coverage](https://img.shields.io/badge/coverage-86%25-success)](https://nhooyrio-websocket-coverage.netlify.app) websocket is a minimal and idiomatic WebSocket library for Go. From 65dfbdd4c1106a9529bbf374aa92ea97a4456c2a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 9 Jan 2021 08:50:17 -0500 Subject: [PATCH 021/152] wasm: Add dial timeout test --- ws_js_test.go | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/ws_js_test.go b/ws_js_test.go index e6be6181..ba98b9a0 100644 --- a/ws_js_test.go +++ b/ws_js_test.go @@ -36,3 +36,19 @@ func TestWasm(t *testing.T) { err = c.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) } + +func TestWasmDialTimeout(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Millisecond) + defer cancel() + + beforeDial := time.Now() + _, _, err := websocket.Dial(ctx, "ws://example.com:9893", &websocket.DialOptions{ + Subprotocols: []string{"echo"}, + }) + assert.Error(t, err) + if time.Since(beforeDial) >= time.Second { + t.Fatal("wasm context dial timeout is not working", time.Since(beforeDial)) + } +} From 8dee580a7f74cf1713400307b4eee514b927870f Mon Sep 17 00:00:00 2001 From: arthmis Date: Fri, 9 Apr 2021 20:03:28 -0400 Subject: [PATCH 022/152] Fix grammar (#295) Co-authored-by: lazypassion <25536767+lazypassion@users.noreply.github.com> --- read.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/read.go b/read.go index ae05cf93..89a00988 100644 --- a/read.go +++ b/read.go @@ -16,7 +16,7 @@ import ( "nhooyr.io/websocket/internal/xsync" ) -// Reader reads from the connection until until there is a WebSocket +// Reader reads from the connection until there is a WebSocket // data message to be read. It will handle ping, pong and close frames as appropriate. // // It returns the type of the message and an io.Reader to read it. From 39bb58838b7a3608dad87e578b45cc0f79880c9f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 02:42:55 -0800 Subject: [PATCH 023/152] README.md: Add note --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index df20c581..28a7ce4e 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ websocket is a minimal and idiomatic WebSocket library for Go. +> **note**: I haven't been responsive for questions/reports on the issue tracker but I do +> read through and I don't believe there are any outstanding bugs. There are certainly +> some nice to haves that I should merge in/figure out but nothing critical. I haven't +> given up on adding new features and cleaning up the code further, just been busy. + ## Install ```bash From 78f81f3a63504ed589c003d3755c133e1b029969 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 03:12:20 -0800 Subject: [PATCH 024/152] REAME: Update note --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index 28a7ce4e..c8cb2271 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. > read through and I don't believe there are any outstanding bugs. There are certainly > some nice to haves that I should merge in/figure out but nothing critical. I haven't > given up on adding new features and cleaning up the code further, just been busy. +> Should anything critical arise, I will fix it. ## Install From 14fb98eba64eeb5e9d06a88b98c47ae924ac82b4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 14:35:01 -0800 Subject: [PATCH 025/152] README: Further update --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c8cb2271..15e3c8e6 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ websocket is a minimal and idiomatic WebSocket library for Go. > **note**: I haven't been responsive for questions/reports on the issue tracker but I do -> read through and I don't believe there are any outstanding bugs. There are certainly -> some nice to haves that I should merge in/figure out but nothing critical. I haven't -> given up on adding new features and cleaning up the code further, just been busy. -> Should anything critical arise, I will fix it. +> read through and there are no outstanding bugs. There are certainly some nice to haves +> that I should merge in/figure out but nothing critical. I haven't given up on adding new +> features and cleaning up the code further, just been busy. Should anything critical +> arise, I will fix it. ## Install From 7fd613642282944805ba6f4f30bd5501b1f74e99 Mon Sep 17 00:00:00 2001 From: Gus Eggert Date: Mon, 30 Jan 2023 08:02:52 -0500 Subject: [PATCH 026/152] Fix dial panic when ctx is nil When the ctx is nil, http.NewRequestWithContext returns a "net/http: nil Context" error and a nil request. In this case, the dial function panics because it assumes the req is never nil. This checks the returning error and returns it, so that callers get an error instead of a panic in that scenario. --- dial.go | 5 ++++- dial_test.go | 22 ++++++++++++++++------ 2 files changed, 20 insertions(+), 7 deletions(-) diff --git a/dial.go b/dial.go index 7a7787ff..0ae0d570 100644 --- a/dial.go +++ b/dial.go @@ -157,7 +157,10 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts return nil, fmt.Errorf("unexpected url scheme: %q", u.Scheme) } - req, _ := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) + if err != nil { + return nil, fmt.Errorf("failed to build HTTP request: %w", err) + } req.Header = opts.HTTPHeader.Clone() req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") diff --git a/dial_test.go b/dial_test.go index 28c255c6..80ba9a3d 100644 --- a/dial_test.go +++ b/dial_test.go @@ -23,10 +23,11 @@ func TestBadDials(t *testing.T) { t.Parallel() testCases := []struct { - name string - url string - opts *DialOptions - rand readerFunc + name string + url string + opts *DialOptions + rand readerFunc + nilCtx bool }{ { name: "badURL", @@ -46,6 +47,11 @@ func TestBadDials(t *testing.T) { return 0, io.EOF }, }, + { + name: "nilContext", + url: "http://localhost", + nilCtx: true, + }, } for _, tc := range testCases { @@ -53,8 +59,12 @@ func TestBadDials(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) - defer cancel() + var ctx context.Context + var cancel func() + if !tc.nilCtx { + ctx, cancel = context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + } if tc.rand == nil { tc.rand = rand.Reader.Read From e2bb5beb7b429305ba25e9bcf9365fda3406737f Mon Sep 17 00:00:00 2001 From: Teddy Okello <37796862+keystroke3@users.noreply.github.com> Date: Sun, 26 Feb 2023 00:25:22 +0300 Subject: [PATCH 027/152] Migrate from deprecated `io/ioutil` --- autobahn_test.go | 8 ++++---- conn_test.go | 5 ++--- dial.go | 5 ++--- dial_test.go | 5 ++--- doc.go | 10 +++++----- examples/chat/chat.go | 4 ++-- read.go | 3 +-- 7 files changed, 18 insertions(+), 22 deletions(-) diff --git a/autobahn_test.go b/autobahn_test.go index e56a4912..1bfb1419 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -6,7 +6,7 @@ import ( "context" "encoding/json" "fmt" - "io/ioutil" + "io" "net" "os" "os/exec" @@ -146,7 +146,7 @@ func wstestCaseCount(ctx context.Context, url string) (cases int, err error) { if err != nil { return 0, err } - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) if err != nil { return 0, err } @@ -161,7 +161,7 @@ func wstestCaseCount(ctx context.Context, url string) (cases int, err error) { } func checkWSTestIndex(t *testing.T, path string) { - wstestOut, err := ioutil.ReadFile(path) + wstestOut, err := os.ReadFile(path) assert.Success(t, err) var indexJSON map[string]map[string]struct { @@ -206,7 +206,7 @@ func unusedListenAddr() (_ string, err error) { } func tempJSONFile(v interface{}) (string, error) { - f, err := ioutil.TempFile("", "temp.json") + f, err := os.CreateTemp("", "temp.json") if err != nil { return "", fmt.Errorf("temp file: %w", err) } diff --git a/conn_test.go b/conn_test.go index c2c41292..d9723686 100644 --- a/conn_test.go +++ b/conn_test.go @@ -7,7 +7,6 @@ import ( "context" "fmt" "io" - "io/ioutil" "net/http" "net/http/httptest" "os" @@ -174,7 +173,7 @@ func TestConn(t *testing.T) { return n2.Close() }) - b, err := ioutil.ReadAll(n1) + b, err := io.ReadAll(n1) assert.Success(t, err) _, err = n1.Read(nil) @@ -205,7 +204,7 @@ func TestConn(t *testing.T) { return nil }) - _, err := ioutil.ReadAll(n1) + _, err := io.ReadAll(n1) assert.Contains(t, err, `unexpected frame type read (expected MessageBinary): MessageText`) select { diff --git a/dial.go b/dial.go index 7a7787ff..2889a37b 100644 --- a/dial.go +++ b/dial.go @@ -10,7 +10,6 @@ import ( "encoding/base64" "fmt" "io" - "io/ioutil" "net/http" "net/url" "strings" @@ -114,9 +113,9 @@ func dial(ctx context.Context, urls string, opts *DialOptions, rand io.Reader) ( }) defer timer.Stop() - b, _ := ioutil.ReadAll(r) + b, _ := io.ReadAll(r) respBody.Close() - resp.Body = ioutil.NopCloser(bytes.NewReader(b)) + resp.Body = io.NopCloser(bytes.NewReader(b)) } }() diff --git a/dial_test.go b/dial_test.go index 28c255c6..89ca3075 100644 --- a/dial_test.go +++ b/dial_test.go @@ -6,7 +6,6 @@ import ( "context" "crypto/rand" "io" - "io/ioutil" "net/http" "net/http/httptest" "strings" @@ -75,7 +74,7 @@ func TestBadDials(t *testing.T) { _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ HTTPClient: mockHTTPClient(func(*http.Request) (*http.Response, error) { return &http.Response{ - Body: ioutil.NopCloser(strings.NewReader("hi")), + Body: io.NopCloser(strings.NewReader("hi")), }, nil }), }) @@ -97,7 +96,7 @@ func TestBadDials(t *testing.T) { return &http.Response{ StatusCode: http.StatusSwitchingProtocols, Header: h, - Body: ioutil.NopCloser(strings.NewReader("hi")), + Body: io.NopCloser(strings.NewReader("hi")), }, nil } diff --git a/doc.go b/doc.go index efa920e3..43c7d92d 100644 --- a/doc.go +++ b/doc.go @@ -16,7 +16,7 @@ // // More documentation at https://nhooyr.io/websocket. // -// Wasm +// # Wasm // // The client side supports compiling to Wasm. // It wraps the WebSocket browser API. @@ -25,8 +25,8 @@ // // Some important caveats to be aware of: // -// - Accept always errors out -// - Conn.Ping is no-op -// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op -// - *http.Response from Dial is &http.Response{} with a 101 status code on success +// - Accept always errors out +// - Conn.Ping is no-op +// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op +// - *http.Response from Dial is &http.Response{} with a 101 status code on success package websocket // import "nhooyr.io/websocket" diff --git a/examples/chat/chat.go b/examples/chat/chat.go index 532e50f5..9d393d87 100644 --- a/examples/chat/chat.go +++ b/examples/chat/chat.go @@ -3,7 +3,7 @@ package main import ( "context" "errors" - "io/ioutil" + "io" "log" "net/http" "sync" @@ -98,7 +98,7 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { return } body := http.MaxBytesReader(w, r.Body, 8192) - msg, err := ioutil.ReadAll(body) + msg, err := io.ReadAll(body) if err != nil { http.Error(w, http.StatusText(http.StatusRequestEntityTooLarge), http.StatusRequestEntityTooLarge) return diff --git a/read.go b/read.go index 89a00988..97a4f987 100644 --- a/read.go +++ b/read.go @@ -8,7 +8,6 @@ import ( "errors" "fmt" "io" - "io/ioutil" "strings" "time" @@ -38,7 +37,7 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { return 0, nil, err } - b, err := ioutil.ReadAll(r) + b, err := io.ReadAll(r) return typ, b, err } From 54809d605a9cd025bcd7336308ec0ec00c4b879b Mon Sep 17 00:00:00 2001 From: Gus Eggert Date: Tue, 7 Mar 2023 09:51:46 -0500 Subject: [PATCH 028/152] Update err message when dial ctx is nil Co-authored-by: Anmol Sethi --- dial.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dial.go b/dial.go index 0ae0d570..8634a5d6 100644 --- a/dial.go +++ b/dial.go @@ -159,7 +159,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { - return nil, fmt.Errorf("failed to build HTTP request: %w", err) + return nil, fmt.Errorf("http.NewRequestWithContext failed: %w", err) } req.Header = opts.HTTPHeader.Clone() req.Header.Set("Connection", "Upgrade") From 6ead6aaf8eb5c3a5c7f533109e0a3baab763289a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 7 Apr 2023 07:59:28 -0700 Subject: [PATCH 029/152] autobahn_test: Use docker to avoid issues with python2 EOL Also ran gofmt on everything. Thanks again @paralin. #334 Co-authored-by: Christian Stewart --- accept.go | 1 + accept_test.go | 1 + autobahn_test.go | 62 +++++++++++++++++++++++++++++------- close.go | 1 + close_test.go | 1 + compress.go | 1 + compress_test.go | 1 + conn.go | 1 + conn_test.go | 1 + dial.go | 1 + dial_test.go | 1 + doc.go | 11 ++++--- export_test.go | 1 + frame_test.go | 1 + internal/test/wstest/echo.go | 2 +- internal/test/wstest/pipe.go | 1 + internal/wsjs/wsjs_js.go | 1 + make.sh | 17 ++++++++++ read.go | 1 + write.go | 1 + 20 files changed, 90 insertions(+), 18 deletions(-) create mode 100755 make.sh diff --git a/accept.go b/accept.go index 542b61e8..d918aab5 100644 --- a/accept.go +++ b/accept.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/accept_test.go b/accept_test.go index 67ece253..ae17c0b4 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/autobahn_test.go b/autobahn_test.go index 7c735a38..4df4b66b 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket_test @@ -33,6 +34,12 @@ var excludedAutobahnCases = []string{ var autobahnCases = []string{"*"} +// Used to run individual test cases. autobahnCases runs only those cases matched +// and not excluded by excludedAutobahnCases. Adding cases here means excludedAutobahnCases +// is niled. +// TODO: +var forceAutobahnCases = []string{} + func TestAutobahn(t *testing.T) { t.Parallel() @@ -43,16 +50,18 @@ func TestAutobahn(t *testing.T) { if os.Getenv("AUTOBAHN") == "fast" { // These are the slow tests. excludedAutobahnCases = append(excludedAutobahnCases, - "9.*", "13.*", "12.*", + "9.*", "12.*", "13.*", ) } - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) + ctx, cancel := context.WithTimeout(context.Background(), time.Hour) defer cancel() - wstestURL, closeFn, err := wstestClientServer(ctx) + wstestURL, closeFn, err := wstestServer(ctx) assert.Success(t, err) - defer closeFn() + defer func() { + assert.Success(t, closeFn()) + }() err = waitWS(ctx, wstestURL) assert.Success(t, err) @@ -100,17 +109,24 @@ func waitWS(ctx context.Context, url string) error { return ctx.Err() } -func wstestClientServer(ctx context.Context) (url string, closeFn func(), err error) { +// TODO: Let docker pick the port and use docker port to find it. +// Does mean we can't use -i but that's fine. +func wstestServer(ctx context.Context) (url string, closeFn func() error, err error) { serverAddr, err := unusedListenAddr() if err != nil { return "", nil, err } + _, serverPort, err := net.SplitHostPort(serverAddr) + if err != nil { + return "", nil, err + } url = "ws://" + serverAddr + const outDir = "ci/out/wstestClientReports" specFile, err := tempJSONFile(map[string]interface{}{ "url": url, - "outdir": "ci/out/wstestClientReports", + "outdir": outDir, "cases": autobahnCases, "exclude-cases": excludedAutobahnCases, }) @@ -118,26 +134,48 @@ func wstestClientServer(ctx context.Context) (url string, closeFn func(), err er return "", nil, fmt.Errorf("failed to write spec: %w", err) } - ctx, cancel := context.WithTimeout(context.Background(), time.Minute*15) + ctx, cancel := context.WithTimeout(ctx, time.Hour) defer func() { if err != nil { cancel() } }() - args := []string{"--mode", "fuzzingserver", "--spec", specFile, + wd, err := os.Getwd() + if err != nil { + return "", nil, err + } + + var args []string + args = append(args, "run", "-i", "--rm", + "-v", fmt.Sprintf("%s:%[1]s", specFile), + "-v", fmt.Sprintf("%s/ci:/ci", wd), + fmt.Sprintf("-p=%s:%s", serverAddr, serverPort), + "crossbario/autobahn-testsuite", + ) + args = append(args, "wstest", "--mode", "fuzzingserver", "--spec", specFile, // Disables some server that runs as part of fuzzingserver mode. // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 "--webport=0", - } - wstest := exec.CommandContext(ctx, "wstest", args...) + ) + fmt.Println(strings.Join(args, " ")) + // TODO: pull image in advance + wstest := exec.CommandContext(ctx, "docker", args...) + // TODO: log to *testing.T + wstest.Stdout = os.Stdout + wstest.Stderr = os.Stderr err = wstest.Start() if err != nil { return "", nil, fmt.Errorf("failed to start wstest: %w", err) } - return url, func() { - wstest.Process.Kill() + // TODO: kill + return url, func() error { + err = wstest.Process.Kill() + if err != nil { + return fmt.Errorf("failed to kill wstest: %w", err) + } + return nil }, nil } diff --git a/close.go b/close.go index d76dc2f4..eab49a8f 100644 --- a/close.go +++ b/close.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/close_test.go b/close_test.go index 00a48d9e..6bf3c256 100644 --- a/close_test.go +++ b/close_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/compress.go b/compress.go index f49d9e5d..68734471 100644 --- a/compress.go +++ b/compress.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/compress_test.go b/compress_test.go index 2c4c896c..7b0e3a68 100644 --- a/compress_test.go +++ b/compress_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/conn.go b/conn.go index beb26cec..25b5a202 100644 --- a/conn.go +++ b/conn.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/conn_test.go b/conn_test.go index 0fbd1740..19961d18 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket_test diff --git a/dial.go b/dial.go index a79b55e6..7e77d6e3 100644 --- a/dial.go +++ b/dial.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/dial_test.go b/dial_test.go index 28c255c6..e5f8ab3d 100644 --- a/dial_test.go +++ b/dial_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/doc.go b/doc.go index efa920e3..a2b873c7 100644 --- a/doc.go +++ b/doc.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js // Package websocket implements the RFC 6455 WebSocket protocol. @@ -16,7 +17,7 @@ // // More documentation at https://nhooyr.io/websocket. // -// Wasm +// # Wasm // // The client side supports compiling to Wasm. // It wraps the WebSocket browser API. @@ -25,8 +26,8 @@ // // Some important caveats to be aware of: // -// - Accept always errors out -// - Conn.Ping is no-op -// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op -// - *http.Response from Dial is &http.Response{} with a 101 status code on success +// - Accept always errors out +// - Conn.Ping is no-op +// - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op +// - *http.Response from Dial is &http.Response{} with a 101 status code on success package websocket // import "nhooyr.io/websocket" diff --git a/export_test.go b/export_test.go index 88b82c9f..d618a154 100644 --- a/export_test.go +++ b/export_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/frame_test.go b/frame_test.go index 76826248..93ad8b5f 100644 --- a/frame_test.go +++ b/frame_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index 8f4e47c8..0938a138 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -21,7 +21,7 @@ func EchoLoop(ctx context.Context, c *websocket.Conn) error { c.SetReadLimit(1 << 30) - ctx, cancel := context.WithTimeout(ctx, time.Minute) + ctx, cancel := context.WithTimeout(ctx, time.Minute*5) defer cancel() b := make([]byte, 32<<10) diff --git a/internal/test/wstest/pipe.go b/internal/test/wstest/pipe.go index f3d4c517..8e1deb47 100644 --- a/internal/test/wstest/pipe.go +++ b/internal/test/wstest/pipe.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package wstest diff --git a/internal/wsjs/wsjs_js.go b/internal/wsjs/wsjs_js.go index 26ffb456..88e8f43f 100644 --- a/internal/wsjs/wsjs_js.go +++ b/internal/wsjs/wsjs_js.go @@ -1,3 +1,4 @@ +//go:build js // +build js // Package wsjs implements typed access to the browser javascript WebSocket API. diff --git a/make.sh b/make.sh new file mode 100755 index 00000000..578203cd --- /dev/null +++ b/make.sh @@ -0,0 +1,17 @@ +#!/bin/sh +set -eu + +cd "$(dirname "$0")" + +fmt() { + go mod tidy + gofmt -s -w . + goimports -w "-local=$(go list -m)" . +} + +if ! command -v wasmbrowsertest >/dev/null; then + go install github.com/agnivade/wasmbrowsertest@latest +fi + +fmt +go test -race --timeout=1h ./... "$@" diff --git a/read.go b/read.go index c4234f20..19727fda 100644 --- a/read.go +++ b/read.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket diff --git a/write.go b/write.go index 58bfdf9a..7921eac9 100644 --- a/write.go +++ b/write.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket From 7c87cb5feb5c276cd7cce95f6b6a8e24cdd206b5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 27 Sep 2023 23:37:06 -0700 Subject: [PATCH 030/152] Fix DOS attack from malicious pongs Cherry picked from master at 129d3035f688f8f1c8a03d65e874e15860d21365 --- conn.go | 2 +- read.go | 5 ++++- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/conn.go b/conn.go index 25b5a202..ab37248e 100644 --- a/conn.go +++ b/conn.go @@ -205,7 +205,7 @@ func (c *Conn) Ping(ctx context.Context) error { } func (c *Conn) ping(ctx context.Context, p string) error { - pong := make(chan struct{}) + pong := make(chan struct{}, 1) c.activePingsMu.Lock() c.activePings[p] = pong diff --git a/read.go b/read.go index 19727fda..98766d7d 100644 --- a/read.go +++ b/read.go @@ -283,7 +283,10 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { pong, ok := c.activePings[string(b)] c.activePingsMu.Unlock() if ok { - close(pong) + select { + case pong <- struct{}{}: + default: + } } return nil } From b8f6512ef2184b9e53248e89b2fed8f79ffb8068 Mon Sep 17 00:00:00 2001 From: arthmis Date: Fri, 9 Apr 2021 20:03:28 -0400 Subject: [PATCH 031/152] Fix grammar (#295) Co-authored-by: lazypassion <25536767+lazypassion@users.noreply.github.com> --- read.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/read.go b/read.go index 98766d7d..87e93460 100644 --- a/read.go +++ b/read.go @@ -17,7 +17,7 @@ import ( "nhooyr.io/websocket/internal/xsync" ) -// Reader reads from the connection until until there is a WebSocket +// Reader reads from the connection until there is a WebSocket // data message to be read. It will handle ping, pong and close frames as appropriate. // // It returns the type of the message and an io.Reader to read it. From 9e84c8936f87c74a0d271f5f5e0a00d883f7c7cf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 02:42:55 -0800 Subject: [PATCH 032/152] README.md: Add note --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 0ae739a0..bc7047d1 100644 --- a/README.md +++ b/README.md @@ -5,6 +5,11 @@ websocket is a minimal and idiomatic WebSocket library for Go. +> **note**: I haven't been responsive for questions/reports on the issue tracker but I do +> read through and I don't believe there are any outstanding bugs. There are certainly +> some nice to haves that I should merge in/figure out but nothing critical. I haven't +> given up on adding new features and cleaning up the code further, just been busy. + ## Install ```bash From de6965b26ed70b37365ba51131ce7eb93c16443e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 03:12:20 -0800 Subject: [PATCH 033/152] REAME: Update note --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index bc7047d1..380fc58e 100644 --- a/README.md +++ b/README.md @@ -9,6 +9,7 @@ websocket is a minimal and idiomatic WebSocket library for Go. > read through and I don't believe there are any outstanding bugs. There are certainly > some nice to haves that I should merge in/figure out but nothing critical. I haven't > given up on adding new features and cleaning up the code further, just been busy. +> Should anything critical arise, I will fix it. ## Install From 9e7b1d5a38230cb42267ad5bb92ed2762d9035ac Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 13 Dec 2022 14:35:01 -0800 Subject: [PATCH 034/152] README: Further update --- README.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 380fc58e..4e73a266 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@ websocket is a minimal and idiomatic WebSocket library for Go. > **note**: I haven't been responsive for questions/reports on the issue tracker but I do -> read through and I don't believe there are any outstanding bugs. There are certainly -> some nice to haves that I should merge in/figure out but nothing critical. I haven't -> given up on adding new features and cleaning up the code further, just been busy. -> Should anything critical arise, I will fix it. +> read through and there are no outstanding bugs. There are certainly some nice to haves +> that I should merge in/figure out but nothing critical. I haven't given up on adding new +> features and cleaning up the code further, just been busy. Should anything critical +> arise, I will fix it. ## Install From 5dd228a41529d7e174c059e465b52eac1d8f1e5b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:14:30 -0700 Subject: [PATCH 035/152] compress.go: Add back comment about Safari compat layer being disabled --- compress.go | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/compress.go b/compress.go index 68734471..a9e1fa35 100644 --- a/compress.go +++ b/compress.go @@ -12,6 +12,12 @@ import ( // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 +// +// A compatibility layer is implemented for the older deflate-frame extension used +// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 +// It will work the same in every way except that we cannot signal to the peer we +// want to use no context takeover on our side, we can only signal that they should. +// But it is currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 type CompressionMode int const ( From b9a4d42a16d442dfbccf2cf52c67311afb32893c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:25:35 -0700 Subject: [PATCH 036/152] LICENSE.txt: Switch to OpenBSD's license --- LICENSE.txt | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/LICENSE.txt b/LICENSE.txt index b5b5fef3..77b5bef6 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,21 +1,13 @@ -MIT License - -Copyright (c) 2018 Anmol Sethi - -Permission is hereby granted, free of charge, to any person obtaining a copy -of this software and associated documentation files (the "Software"), to deal -in the Software without restriction, including without limitation the rights -to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -copies of the Software, and to permit persons to whom the Software is -furnished to do so, subject to the following conditions: - -The above copyright notice and this permission notice shall be included in all -copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. +Copyright (c) 2023 Anmol Sethi + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. From a374f19a700ae45ed57d8e391f769ef86ddab0fe Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:28:30 -0700 Subject: [PATCH 037/152] .github: Delete CODEOWNERS --- .github/CODEOWNERS | 1 - 1 file changed, 1 deletion(-) delete mode 100644 .github/CODEOWNERS diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS deleted file mode 100644 index d2eae33e..00000000 --- a/.github/CODEOWNERS +++ /dev/null @@ -1 +0,0 @@ -* @nhooyr From c45cd4cdecad5b6f817860e4d0aa428ac5e6faec Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:28:36 -0700 Subject: [PATCH 038/152] ci/container: Fix for newer Go --- ci/container/Dockerfile | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile index 0c6c2a54..e2721b9b 100644 --- a/ci/container/Dockerfile +++ b/ci/container/Dockerfile @@ -4,11 +4,11 @@ RUN apt-get update RUN apt-get install -y npm shellcheck chromium ENV GO111MODULE=on -RUN go get golang.org/x/tools/cmd/goimports -RUN go get mvdan.cc/sh/v3/cmd/shfmt -RUN go get golang.org/x/tools/cmd/stringer -RUN go get golang.org/x/lint/golint -RUN go get github.com/agnivade/wasmbrowsertest +RUN go install golang.org/x/tools/cmd/goimports@latest +RUN go install mvdan.cc/sh/v3/cmd/shfmt@latest +RUN go install golang.org/x/tools/cmd/stringer@latest +RUN go install golang.org/x/lint/golint@latest +RUN go install github.com/agnivade/wasmbrowsertest@latest RUN npm --unsafe-perm=true install -g prettier RUN npm --unsafe-perm=true install -g netlify-cli From 118ea682a3ac882657ee11d7a2539a186a6766af Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 28 Sep 2023 08:36:13 -0700 Subject: [PATCH 039/152] ci: Fixes Credits to @maggie44 for making me add staticcheck. See #407 Co-authored-by: maggie0002 <64841595+maggie0002@users.noreply.github.com> --- .github/workflows/ci.yaml | 39 ------------------------------ .github/workflows/ci.yml | 39 ++++++++++++++++++++++++++++++ ci/all.sh | 12 ---------- ci/container/Dockerfile | 14 ----------- ci/fmt.sh | 50 ++++++++++++--------------------------- ci/lint.sh | 24 +++++++++---------- ci/test.sh | 33 +++++++++----------------- examples/chat/index.css | 8 +++---- examples/chat/index.html | 2 +- examples/chat/index.js | 36 ++++++++++++++-------------- make.sh | 18 ++++---------- 11 files changed, 103 insertions(+), 172 deletions(-) delete mode 100644 .github/workflows/ci.yaml create mode 100644 .github/workflows/ci.yml delete mode 100755 ci/all.sh delete mode 100644 ci/container/Dockerfile diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml deleted file mode 100644 index 3d9829ef..00000000 --- a/.github/workflows/ci.yaml +++ /dev/null @@ -1,39 +0,0 @@ -name: ci - -on: [push, pull_request] - -jobs: - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Run ./ci/fmt.sh - uses: ./ci/container - with: - args: ./ci/fmt.sh - - lint: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Run ./ci/lint.sh - uses: ./ci/container - with: - args: ./ci/lint.sh - - test: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v1 - - name: Run ./ci/test.sh - uses: ./ci/container - with: - args: ./ci/test.sh - env: - NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }} - NETLIFY_SITE_ID: 9b3ee4dc-8297-4774-b4b9-a61561fbbce7 - - name: Upload coverage.html - uses: actions/upload-artifact@v2 - with: - name: coverage.html - path: ./ci/out/coverage.html diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..f31ea711 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,39 @@ +name: ci +on: [push, pull_request] +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.sha }} + cancel-in-progress: true + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: ./ci/fmt.sh + + lint: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - run: go version + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: ./ci/lint.sh + + test: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: ./ci/test.sh + - uses: actions/upload-artifact@v2 + if: always() + with: + name: coverage.html + path: ./ci/out/coverage.html diff --git a/ci/all.sh b/ci/all.sh deleted file mode 100755 index 1ee7640f..00000000 --- a/ci/all.sh +++ /dev/null @@ -1,12 +0,0 @@ -#!/usr/bin/env bash -set -euo pipefail - -main() { - cd "$(dirname "$0")/.." - - ./ci/fmt.sh - ./ci/lint.sh - ./ci/test.sh "$@" -} - -main "$@" diff --git a/ci/container/Dockerfile b/ci/container/Dockerfile deleted file mode 100644 index e2721b9b..00000000 --- a/ci/container/Dockerfile +++ /dev/null @@ -1,14 +0,0 @@ -FROM golang - -RUN apt-get update -RUN apt-get install -y npm shellcheck chromium - -ENV GO111MODULE=on -RUN go install golang.org/x/tools/cmd/goimports@latest -RUN go install mvdan.cc/sh/v3/cmd/shfmt@latest -RUN go install golang.org/x/tools/cmd/stringer@latest -RUN go install golang.org/x/lint/golint@latest -RUN go install github.com/agnivade/wasmbrowsertest@latest - -RUN npm --unsafe-perm=true install -g prettier -RUN npm --unsafe-perm=true install -g netlify-cli diff --git a/ci/fmt.sh b/ci/fmt.sh index b34f1438..0d902732 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -1,38 +1,18 @@ -#!/usr/bin/env bash -set -euo pipefail +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." -main() { - cd "$(dirname "$0")/.." +go mod tidy +gofmt -w -s . +go run golang.org/x/tools/cmd/goimports@latest -w "-local=$(go list -m)" . - go mod tidy - gofmt -w -s . - goimports -w "-local=$(go list -m)" . +npx prettier@3.0.3 \ + --write \ + --log-level=warn \ + --print-width=90 \ + --no-semi \ + --single-quote \ + --arrow-parens=avoid \ + $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") - prettier \ - --write \ - --print-width=120 \ - --no-semi \ - --trailing-comma=all \ - --loglevel=warn \ - --arrow-parens=avoid \ - $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") - shfmt -i 2 -w -s -sr $(git ls-files "*.sh") - - stringer -type=opcode,MessageType,StatusCode -output=stringer.go - - if [[ ${CI-} ]]; then - assert_no_changes - fi -} - -assert_no_changes() { - if [[ $(git ls-files --other --modified --exclude-standard) ]]; then - git -c color.ui=always --no-pager diff - echo - echo "Please run the following locally:" - echo " ./ci/fmt.sh" - exit 1 - fi -} - -main "$@" +go run golang.org/x/tools/cmd/stringer@latest -type=opcode,MessageType,StatusCode -output=stringer.go diff --git a/ci/lint.sh b/ci/lint.sh index e1053d13..a8ab3027 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -1,16 +1,14 @@ -#!/usr/bin/env bash -set -euo pipefail +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." -main() { - cd "$(dirname "$0")/.." +go vet ./... +GOOS=js GOARCH=wasm go vet ./... - go vet ./... - GOOS=js GOARCH=wasm go vet ./... +go install golang.org/x/lint/golint@latest +golint -set_exit_status ./... +GOOS=js GOARCH=wasm golint -set_exit_status ./... - golint -set_exit_status ./... - GOOS=js GOARCH=wasm golint -set_exit_status ./... - - shellcheck --exclude=SC2046 $(git ls-files "*.sh") -} - -main "$@" +go install honnef.co/go/tools/cmd/staticcheck@latest +staticcheck ./... +GOOS=js GOARCH=wasm staticcheck ./... diff --git a/ci/test.sh b/ci/test.sh index bd68b80e..1b3d6cc3 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -1,25 +1,14 @@ -#!/usr/bin/env bash -set -euo pipefail +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." -main() { - cd "$(dirname "$0")/.." +go install github.com/agnivade/wasmbrowsertest@latest +go test --race --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... +sed -i.bak '/stringer\.go/d' ci/out/coverage.prof +sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof +sed -i.bak '/examples/d' ci/out/coverage.prof - go test -timeout=30m -covermode=atomic -coverprofile=ci/out/coverage.prof -coverpkg=./... "$@" ./... - sed -i.bak '/stringer\.go/d' ci/out/coverage.prof - sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof - sed -i.bak '/examples/d' ci/out/coverage.prof +# Last line is the total coverage. +go tool cover -func ci/out/coverage.prof | tail -n1 - # Last line is the total coverage. - go tool cover -func ci/out/coverage.prof | tail -n1 - - go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html - - if [[ ${CI-} && ${GITHUB_REF-} == *master ]]; then - local deployDir - deployDir="$(mktemp -d)" - cp ci/out/coverage.html "$deployDir/index.html" - netlify deploy --prod "--dir=$deployDir" - fi -} - -main "$@" +go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html diff --git a/examples/chat/index.css b/examples/chat/index.css index 73a8e0f3..ce27c378 100644 --- a/examples/chat/index.css +++ b/examples/chat/index.css @@ -54,7 +54,7 @@ body { margin: 0 0 0 10px; } -#publish-form input[type="text"] { +#publish-form input[type='text'] { flex-grow: 1; -moz-appearance: none; @@ -64,7 +64,7 @@ body { border: 1px solid #ccc; } -#publish-form input[type="submit"] { +#publish-form input[type='submit'] { color: white; background-color: black; border-radius: 5px; @@ -72,10 +72,10 @@ body { border: none; } -#publish-form input[type="submit"]:hover { +#publish-form input[type='submit']:hover { background-color: red; } -#publish-form input[type="submit"]:active { +#publish-form input[type='submit']:active { background-color: red; } diff --git a/examples/chat/index.html b/examples/chat/index.html index 76ae8370..64edd286 100644 --- a/examples/chat/index.html +++ b/examples/chat/index.html @@ -1,4 +1,4 @@ - + diff --git a/examples/chat/index.js b/examples/chat/index.js index 5868e7ca..2efca013 100644 --- a/examples/chat/index.js +++ b/examples/chat/index.js @@ -6,21 +6,21 @@ function dial() { const conn = new WebSocket(`ws://${location.host}/subscribe`) - conn.addEventListener("close", ev => { + conn.addEventListener('close', ev => { appendLog(`WebSocket Disconnected code: ${ev.code}, reason: ${ev.reason}`, true) if (ev.code !== 1001) { - appendLog("Reconnecting in 1s", true) + appendLog('Reconnecting in 1s', true) setTimeout(dial, 1000) } }) - conn.addEventListener("open", ev => { - console.info("websocket connected") + conn.addEventListener('open', ev => { + console.info('websocket connected') }) // This is where we handle messages received. - conn.addEventListener("message", ev => { - if (typeof ev.data !== "string") { - console.error("unexpected message type", typeof ev.data) + conn.addEventListener('message', ev => { + if (typeof ev.data !== 'string') { + console.error('unexpected message type', typeof ev.data) return } const p = appendLog(ev.data) @@ -32,38 +32,38 @@ } dial() - const messageLog = document.getElementById("message-log") - const publishForm = document.getElementById("publish-form") - const messageInput = document.getElementById("message-input") + const messageLog = document.getElementById('message-log') + const publishForm = document.getElementById('publish-form') + const messageInput = document.getElementById('message-input') // appendLog appends the passed text to messageLog. function appendLog(text, error) { - const p = document.createElement("p") + const p = document.createElement('p') // Adding a timestamp to each message makes the log easier to read. p.innerText = `${new Date().toLocaleTimeString()}: ${text}` if (error) { - p.style.color = "red" - p.style.fontStyle = "bold" + p.style.color = 'red' + p.style.fontStyle = 'bold' } messageLog.append(p) return p } - appendLog("Submit a message to get started!") + appendLog('Submit a message to get started!') // onsubmit publishes the message from the user when the form is submitted. publishForm.onsubmit = async ev => { ev.preventDefault() const msg = messageInput.value - if (msg === "") { + if (msg === '') { return } - messageInput.value = "" + messageInput.value = '' expectingMessage = true try { - const resp = await fetch("/publish", { - method: "POST", + const resp = await fetch('/publish', { + method: 'POST', body: msg, }) if (resp.status !== 202) { diff --git a/make.sh b/make.sh index 578203cd..6f5d1f57 100755 --- a/make.sh +++ b/make.sh @@ -1,17 +1,7 @@ #!/bin/sh set -eu +cd -- "$(dirname "$0")" -cd "$(dirname "$0")" - -fmt() { - go mod tidy - gofmt -s -w . - goimports -w "-local=$(go list -m)" . -} - -if ! command -v wasmbrowsertest >/dev/null; then - go install github.com/agnivade/wasmbrowsertest@latest -fi - -fmt -go test -race --timeout=1h ./... "$@" +./ci/fmt.sh +./ci/lint.sh +./ci/test.sh From 8d3d892cf636d3465b1c7424ca91ffc1720db172 Mon Sep 17 00:00:00 2001 From: Jacalz Date: Sun, 12 Mar 2023 15:39:19 +0100 Subject: [PATCH 040/152] Update Go module version to 1.18 Fixes https://github.com/nhooyr/websocket/issues/359 --- go.mod | 22 +++++++++++++++++++--- go.sum | 1 - 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/go.mod b/go.mod index d4bca923..ad9bc045 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,30 @@ module nhooyr.io/websocket -go 1.13 +go 1.18 require ( github.com/gin-gonic/gin v1.6.3 - github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect - github.com/gobwas/pool v0.2.0 // indirect github.com/gobwas/ws v1.0.2 github.com/golang/protobuf v1.3.5 github.com/google/go-cmp v0.4.0 github.com/gorilla/websocket v1.4.1 golang.org/x/time v0.0.0-20191024005414-555d28b269f0 ) + +require ( + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.13.0 // indirect + github.com/go-playground/universal-translator v0.17.0 // indirect + github.com/go-playground/validator/v10 v10.2.0 // indirect + github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect + github.com/gobwas/pool v0.2.0 // indirect + github.com/json-iterator/go v1.1.9 // indirect + github.com/leodido/go-urn v1.2.0 // indirect + github.com/mattn/go-isatty v0.0.12 // indirect + github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect + github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect + github.com/ugorji/go/codec v1.1.7 // indirect + golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect + golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect + gopkg.in/yaml.v2 v2.2.8 // indirect +) diff --git a/go.sum b/go.sum index 1344e958..75854444 100644 --- a/go.sum +++ b/go.sum @@ -43,7 +43,6 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+ github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= From 4188bcfe6f341ddc7c5d298d4f776cd737113204 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 03:55:09 -0700 Subject: [PATCH 041/152] go.mod: Regenerate --- go.mod | 51 +++++++++++++--------- go.sum | 131 ++++++++++++++++++++++++++++++++++++--------------------- 2 files changed, 115 insertions(+), 67 deletions(-) diff --git a/go.mod b/go.mod index ad9bc045..c7e7dfd2 100644 --- a/go.mod +++ b/go.mod @@ -3,28 +3,39 @@ module nhooyr.io/websocket go 1.18 require ( - github.com/gin-gonic/gin v1.6.3 - github.com/gobwas/ws v1.0.2 - github.com/golang/protobuf v1.3.5 - github.com/google/go-cmp v0.4.0 - github.com/gorilla/websocket v1.4.1 - golang.org/x/time v0.0.0-20191024005414-555d28b269f0 + github.com/gin-gonic/gin v1.9.1 + github.com/gobwas/ws v1.3.0 + github.com/golang/protobuf v1.5.3 + github.com/google/go-cmp v0.5.9 + github.com/gorilla/websocket v1.5.0 + golang.org/x/time v0.3.0 ) require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.13.0 // indirect - github.com/go-playground/universal-translator v0.17.0 // indirect - github.com/go-playground/validator/v10 v10.2.0 // indirect - github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee // indirect - github.com/gobwas/pool v0.2.0 // indirect - github.com/json-iterator/go v1.1.9 // indirect - github.com/leodido/go-urn v1.2.0 // indirect - github.com/mattn/go-isatty v0.0.12 // indirect - github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 // indirect - github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 // indirect - github.com/ugorji/go/codec v1.1.7 // indirect - golang.org/x/sys v0.0.0-20200116001909-b77594299b42 // indirect - golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 // indirect - gopkg.in/yaml.v2 v2.2.8 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/go.sum b/go.sum index 75854444..78c452e4 100644 --- a/go.sum +++ b/go.sum @@ -1,61 +1,98 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-playground/assert/v2 v2.0.1 h1:MsBgLAaY856+nPRTKrp3/OZK38U/wa0CcBYNjji3q3A= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5 h1:F768QJ1E9tib+q5Sc8MkdJi1RxLTbRcTf8LJV56aRls= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= +github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= +github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.4.1 h1:q7AeDBpnBk8AogcD4DSag/Ukw/KV+YhzLj2bP5HvKCM= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/json-iterator/go v1.1.9 h1:9yzud/Ht36ygwatGx56VwCZtlI/2AD15T1X2sjSuGns= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-isatty v0.0.12 h1:wuysRhFDzyxgEmMf5xjvJ2M9dZoWAXNNr5LSBS7uHXY= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421 h1:ZqeYNhU3OHLH3mGKHDcjJRFFRrJa6eAM5H+CtDdOsPc= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742 h1:Esafd1046DLDQ0W1YjYsBW+p8U2u7vzgW2SQVmlNazg= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42 h1:vEOn+mP2zCOVzKckCZy6YsCtDblrpj/w7B9nxGNELpg= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0 h1:/5xXl8Y5W96D+TtHSlonuFqGHIWVuyCkGJLwGh9JJFs= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From 1c90f47e4929302ce89b3e7c5778eeac5a3ae7ab Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 04:02:03 -0700 Subject: [PATCH 042/152] ci.yml: Fix --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f31ea711..8b88e81c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,8 +32,7 @@ jobs: with: go-version-file: ./go.mod - run: ./ci/test.sh - - uses: actions/upload-artifact@v2 - if: always() + - uses: actions/upload-artifact@v3 with: name: coverage.html path: ./ci/out/coverage.html From 2a5a56660c4f17cc12c8532ced8869f25a310ad8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 04:08:26 -0700 Subject: [PATCH 043/152] go.mod: Upgrade to Go 1.19 --- go.mod | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/go.mod b/go.mod index c7e7dfd2..50c873bf 100644 --- a/go.mod +++ b/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket -go 1.18 +go 1.19 require ( github.com/gin-gonic/gin v1.9.1 From e1e65adca29fa3d2989286c16d737c9bc276779a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 04:11:09 -0700 Subject: [PATCH 044/152] daily.yml: Add to run AUTOBAHN tests daily --- .github/workflows/daily.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 .github/workflows/daily.yml diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml new file mode 100644 index 00000000..cbac574d --- /dev/null +++ b/.github/workflows/daily.yml @@ -0,0 +1,22 @@ +name: daily +on: + workflow_dispatch: + schedule: + - cron: '42 0 * * *' # daily at 00:42 +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: true + +jobs: + ci: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: AUTOBAHN=1 ./ci/test.sh + - uses: actions/upload-artifact@v3 + with: + name: coverage.html + path: ./ci/out/coverage.html From 75bf907768b38735aaa002044e81fad7c443ba43 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 08:25:37 -0700 Subject: [PATCH 045/152] autobahn_test.go: Pull image before starting container --- autobahn_test.go | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/autobahn_test.go b/autobahn_test.go index 4df4b66b..23723b51 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -112,6 +112,8 @@ func waitWS(ctx context.Context, url string) error { // TODO: Let docker pick the port and use docker port to find it. // Does mean we can't use -i but that's fine. func wstestServer(ctx context.Context) (url string, closeFn func() error, err error) { + defer errd.Wrap(&err, "failed to start autobahn wstest server") + serverAddr, err := unusedListenAddr() if err != nil { return "", nil, err @@ -141,6 +143,15 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er } }() + dockerPull := exec.CommandContext(ctx, "docker", "pull", "crossbario/autobahn-testsuite") + // TODO: log to *testing.T + dockerPull.Stdout = os.Stdout + dockerPull.Stderr = os.Stderr + err = dockerPull.Run() + if err != nil { + return "", nil, fmt.Errorf("failed to pull docker image: %w", err) + } + wd, err := os.Getwd() if err != nil { return "", nil, err @@ -159,7 +170,6 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er "--webport=0", ) fmt.Println(strings.Join(args, " ")) - // TODO: pull image in advance wstest := exec.CommandContext(ctx, "docker", args...) // TODO: log to *testing.T wstest.Stdout = os.Stdout From 4ab2f5421083225550ef44000699a0d6e899983c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 10 Oct 2023 08:34:49 -0700 Subject: [PATCH 046/152] conn_test: Remove ioutil --- conn_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/conn_test.go b/conn_test.go index f474ae0c..639666b1 100644 --- a/conn_test.go +++ b/conn_test.go @@ -223,7 +223,7 @@ func TestConn(t *testing.T) { return n2.Close() }) - b, err := ioutil.ReadAll(n1) + b, err := io.ReadAll(n1) assert.Success(t, err) _, err = n1.Read(nil) From e9d08816010996a14241f008ac097c5621bd1f30 Mon Sep 17 00:00:00 2001 From: Simon Ser Date: Wed, 19 May 2021 23:52:23 +0200 Subject: [PATCH 047/152] Use net.ErrClosed Go 1.16 has introduced net.ErrClosed, which should be returned/wrapped when an I/O call is performed on a network connection which has already been closed. This is useful to avoid cluttering logs with messages like "failed to close WebSocket: already wrote close". Closes: https://github.com/nhooyr/websocket/issues/286 --- close.go | 4 +--- close_go113.go | 9 +++++++++ close_go116.go | 9 +++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) create mode 100644 close_go113.go create mode 100644 close_go116.go diff --git a/close.go b/close.go index eab49a8f..1e13ca73 100644 --- a/close.go +++ b/close.go @@ -119,15 +119,13 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return nil } -var errAlreadyWroteClose = errors.New("already wrote close") - func (c *Conn) writeClose(code StatusCode, reason string) error { c.closeMu.Lock() wroteClose := c.wroteClose c.wroteClose = true c.closeMu.Unlock() if wroteClose { - return errAlreadyWroteClose + return errClosed } ce := CloseError{ diff --git a/close_go113.go b/close_go113.go new file mode 100644 index 00000000..4f586dcb --- /dev/null +++ b/close_go113.go @@ -0,0 +1,9 @@ +// +build !go1.16 + +package websocket + +import ( + "errors" +) + +var errClosed = errors.New("use of closed network connection") diff --git a/close_go116.go b/close_go116.go new file mode 100644 index 00000000..0a6e5f15 --- /dev/null +++ b/close_go116.go @@ -0,0 +1,9 @@ +// +build go1.16 + +package websocket + +import ( + "net" +) + +var errClosed = net.ErrClosed From e3050279d59cc6896b8e056ac1b1ec3eca484176 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:25:59 -0700 Subject: [PATCH 048/152] dial.go: Clarify http.NewRequestWithContext error --- dial.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/dial.go b/dial.go index ac05cba6..4b2b7b62 100644 --- a/dial.go +++ b/dial.go @@ -166,7 +166,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts req, err := http.NewRequestWithContext(ctx, "GET", u.String(), nil) if err != nil { - return nil, fmt.Errorf("http.NewRequestWithContext failed: %w", err) + return nil, fmt.Errorf("failed to create new http request: %w", err) } req.Header = opts.HTTPHeader.Clone() req.Header.Set("Connection", "Upgrade") From ac385120c6e34fa6584f3856d5db949a21bbb65e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:29:58 -0700 Subject: [PATCH 049/152] wspb: Remove The library we're currently using for protobufs is deprecated. Doesn't belong in the library core anyway. Closes #311 Updates #297 --- conn_test.go | 21 --------------- wspb/wspb.go | 73 ---------------------------------------------------- 2 files changed, 94 deletions(-) delete mode 100644 wspb/wspb.go diff --git a/conn_test.go b/conn_test.go index 639666b1..a3f3d787 100644 --- a/conn_test.go +++ b/conn_test.go @@ -17,8 +17,6 @@ import ( "time" "github.com/gin-gonic/gin" - "github.com/golang/protobuf/ptypes" - "github.com/golang/protobuf/ptypes/duration" "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" @@ -27,7 +25,6 @@ import ( "nhooyr.io/websocket/internal/test/xrand" "nhooyr.io/websocket/internal/xsync" "nhooyr.io/websocket/wsjson" - "nhooyr.io/websocket/wspb" ) func TestConn(t *testing.T) { @@ -267,24 +264,6 @@ func TestConn(t *testing.T) { err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) }) - - t.Run("wspb", func(t *testing.T) { - tt, c1, c2 := newConnTest(t, nil, nil) - - tt.goEchoLoop(c2) - - exp := ptypes.DurationProto(100) - err := wspb.Write(tt.ctx, c1, exp) - assert.Success(t, err) - - act := &duration.Duration{} - err = wspb.Read(tt.ctx, c1, act) - assert.Success(t, err) - assert.Equal(t, "read msg", exp, act) - - err = c1.Close(websocket.StatusNormalClosure, "") - assert.Success(t, err) - }) } func TestWasm(t *testing.T) { diff --git a/wspb/wspb.go b/wspb/wspb.go deleted file mode 100644 index e43042d5..00000000 --- a/wspb/wspb.go +++ /dev/null @@ -1,73 +0,0 @@ -// Package wspb provides helpers for reading and writing protobuf messages. -package wspb // import "nhooyr.io/websocket/wspb" - -import ( - "bytes" - "context" - "fmt" - - "github.com/golang/protobuf/proto" - - "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/errd" -) - -// Read reads a protobuf message from c into v. -// It will reuse buffers in between calls to avoid allocations. -func Read(ctx context.Context, c *websocket.Conn, v proto.Message) error { - return read(ctx, c, v) -} - -func read(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { - defer errd.Wrap(&err, "failed to read protobuf message") - - typ, r, err := c.Reader(ctx) - if err != nil { - return err - } - - if typ != websocket.MessageBinary { - c.Close(websocket.StatusUnsupportedData, "expected binary message") - return fmt.Errorf("expected binary message for protobuf but got: %v", typ) - } - - b := bpool.Get() - defer bpool.Put(b) - - _, err = b.ReadFrom(r) - if err != nil { - return err - } - - err = proto.Unmarshal(b.Bytes(), v) - if err != nil { - c.Close(websocket.StatusInvalidFramePayloadData, "failed to unmarshal protobuf") - return fmt.Errorf("failed to unmarshal protobuf: %w", err) - } - - return nil -} - -// Write writes the protobuf message v to c. -// It will reuse buffers in between calls to avoid allocations. -func Write(ctx context.Context, c *websocket.Conn, v proto.Message) error { - return write(ctx, c, v) -} - -func write(ctx context.Context, c *websocket.Conn, v proto.Message) (err error) { - defer errd.Wrap(&err, "failed to write protobuf message") - - b := bpool.Get() - pb := proto.NewBuffer(b.Bytes()) - defer func() { - bpool.Put(bytes.NewBuffer(pb.Bytes())) - }() - - err = pb.Marshal(v) - if err != nil { - return fmt.Errorf("failed to marshal protobuf: %w", err) - } - - return c.Write(ctx, websocket.MessageBinary, pb.Bytes()) -} From 9b5a15bfc3b8b016eda073c01fec299ea84f8804 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:33:13 -0700 Subject: [PATCH 050/152] close_goXXX.go: fmt --- close_go113.go | 1 + close_go116.go | 1 + 2 files changed, 2 insertions(+) diff --git a/close_go113.go b/close_go113.go index 4f586dcb..fb226475 100644 --- a/close_go113.go +++ b/close_go113.go @@ -1,3 +1,4 @@ +//go:build !go1.16 // +build !go1.16 package websocket diff --git a/close_go116.go b/close_go116.go index 0a6e5f15..2724e0ca 100644 --- a/close_go116.go +++ b/close_go116.go @@ -1,3 +1,4 @@ +//go:build go1.16 // +build go1.16 package websocket From a633a10fb558ad5b93247ca57f45480f643f7ce6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:49:40 -0700 Subject: [PATCH 051/152] lint.sh: Pass --- accept.go | 39 ++-------------------------------- autobahn_test.go | 2 +- close_go113.go | 3 +-- close_go116.go | 3 +-- compress.go | 6 +++--- frame.go | 2 ++ frame_test.go | 6 ++++-- go.mod | 1 - go.sum | 3 --- internal/test/assert/assert.go | 3 +-- internal/wsjs/wsjs_js.go | 2 -- netconn.go | 4 ++-- ws_js.go | 26 ++++++++++++++++++++--- 13 files changed, 40 insertions(+), 60 deletions(-) diff --git a/accept.go b/accept.go index d918aab5..ff2033e7 100644 --- a/accept.go +++ b/accept.go @@ -245,11 +245,10 @@ func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionM for _, ext := range websocketExtensions(r.Header) { switch ext.name { + // We used to implement x-webkit-deflate-fram too but Safari has bugs. + // See https://github.com/nhooyr/websocket/issues/218 case "permessage-deflate": return acceptDeflate(w, ext, mode) - // Disabled for now, see https://github.com/nhooyr/websocket/issues/218 - // case "x-webkit-deflate-frame": - // return acceptWebkitDeflate(w, ext, mode) } } return nil, nil @@ -283,40 +282,6 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi return copts, nil } -func acceptWebkitDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { - copts := mode.opts() - // The peer must explicitly request it. - copts.serverNoContextTakeover = false - - for _, p := range ext.params { - if p == "no_context_takeover" { - copts.serverNoContextTakeover = true - continue - } - - // We explicitly fail on x-webkit-deflate-frame's max_window_bits parameter instead - // of ignoring it as the draft spec is unclear. It says the server can ignore it - // but the server has no way of signalling to the client it was ignored as the parameters - // are set one way. - // Thus us ignoring it would make the client think we understood it which would cause issues. - // See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06#section-4.1 - // - // Either way, we're only implementing this for webkit which never sends the max_window_bits - // parameter so we don't need to worry about it. - err := fmt.Errorf("unsupported x-webkit-deflate-frame parameter: %q", p) - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, err - } - - s := "x-webkit-deflate-frame" - if copts.clientNoContextTakeover { - s += "; no_context_takeover" - } - w.Header().Set("Sec-WebSocket-Extensions", s) - - return copts, nil -} - func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool { for _, t := range headerTokens(h, key) { if strings.EqualFold(t, token) { diff --git a/autobahn_test.go b/autobahn_test.go index 8100c37f..41fae555 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -38,7 +38,7 @@ var autobahnCases = []string{"*"} // and not excluded by excludedAutobahnCases. Adding cases here means excludedAutobahnCases // is niled. // TODO: -var forceAutobahnCases = []string{} +// var forceAutobahnCases = []string{} func TestAutobahn(t *testing.T) { t.Parallel() diff --git a/close_go113.go b/close_go113.go index fb226475..caf1b89e 100644 --- a/close_go113.go +++ b/close_go113.go @@ -1,5 +1,4 @@ -//go:build !go1.16 -// +build !go1.16 +//go:build !go1.16 && !js package websocket diff --git a/close_go116.go b/close_go116.go index 2724e0ca..9d986109 100644 --- a/close_go116.go +++ b/close_go116.go @@ -1,5 +1,4 @@ -//go:build go1.16 -// +build go1.16 +//go:build go1.16 && !js package websocket diff --git a/compress.go b/compress.go index a9e1fa35..e6722fc7 100644 --- a/compress.go +++ b/compress.go @@ -201,9 +201,9 @@ func (sw *slidingWindow) init(n int) { } p := slidingWindowPool(n) - buf, ok := p.Get().([]byte) + buf, ok := p.Get().(*[]byte) if ok { - sw.buf = buf[:0] + sw.buf = (*buf)[:0] } else { sw.buf = make([]byte, 0, n) } @@ -215,7 +215,7 @@ func (sw *slidingWindow) close() { } swPoolMu.Lock() - swPool[cap(sw.buf)].Put(sw.buf) + swPool[cap(sw.buf)].Put(&sw.buf) swPoolMu.Unlock() sw.buf = nil } diff --git a/frame.go b/frame.go index 2a036f94..351632fd 100644 --- a/frame.go +++ b/frame.go @@ -1,3 +1,5 @@ +//go:build !js + package websocket import ( diff --git a/frame_test.go b/frame_test.go index 93ad8b5f..2f4f2e25 100644 --- a/frame_test.go +++ b/frame_test.go @@ -55,7 +55,7 @@ func TestHeader(t *testing.T) { r := rand.New(rand.NewSource(time.Now().UnixNano())) randBool := func() bool { - return r.Intn(1) == 0 + return r.Intn(2) == 0 } for i := 0; i < 10000; i++ { @@ -67,9 +67,11 @@ func TestHeader(t *testing.T) { opcode: opcode(r.Intn(16)), masked: randBool(), - maskKey: r.Uint32(), payloadLength: r.Int63(), } + if h.masked { + h.maskKey = r.Uint32() + } testHeader(t, h) } diff --git a/go.mod b/go.mod index 50c873bf..95a1df92 100644 --- a/go.mod +++ b/go.mod @@ -5,7 +5,6 @@ go 1.19 require ( github.com/gin-gonic/gin v1.9.1 github.com/gobwas/ws v1.3.0 - github.com/golang/protobuf v1.5.3 github.com/google/go-cmp v0.5.9 github.com/gorilla/websocket v1.5.0 golang.org/x/time v0.3.0 diff --git a/go.sum b/go.sum index 78c452e4..dc4743dd 100644 --- a/go.sum +++ b/go.sum @@ -29,8 +29,6 @@ github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/K github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -87,7 +85,6 @@ golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index 6eaf7fc3..e37e9573 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -6,7 +6,6 @@ import ( "strings" "testing" - "github.com/golang/protobuf/proto" "github.com/google/go-cmp/cmp" "github.com/google/go-cmp/cmp/cmpopts" ) @@ -15,7 +14,7 @@ import ( func Diff(v1, v2 interface{}) string { return cmp.Diff(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { return true - }), cmp.Comparer(proto.Equal)) + })) } // Equal asserts exp == act. diff --git a/internal/wsjs/wsjs_js.go b/internal/wsjs/wsjs_js.go index 88e8f43f..11eb59cb 100644 --- a/internal/wsjs/wsjs_js.go +++ b/internal/wsjs/wsjs_js.go @@ -119,8 +119,6 @@ func (c WebSocket) OnMessage(fn func(m MessageEvent)) (remove func()) { Data: data, } fn(me) - - return }) } diff --git a/netconn.go b/netconn.go index aea1a02d..4af6c202 100644 --- a/netconn.go +++ b/netconn.go @@ -200,7 +200,7 @@ func (nc *netConn) SetWriteDeadline(t time.Time) error { if t.IsZero() { nc.writeTimer.Stop() } else { - nc.writeTimer.Reset(t.Sub(time.Now())) + nc.writeTimer.Reset(time.Until(t)) } return nil } @@ -210,7 +210,7 @@ func (nc *netConn) SetReadDeadline(t time.Time) error { if t.IsZero() { nc.readTimer.Stop() } else { - nc.readTimer.Reset(t.Sub(time.Now())) + nc.readTimer.Reset(time.Until(t)) } return nil } diff --git a/ws_js.go b/ws_js.go index d1361328..3248933c 100644 --- a/ws_js.go +++ b/ws_js.go @@ -18,6 +18,26 @@ import ( "nhooyr.io/websocket/internal/xsync" ) +// opcode represents a WebSocket opcode. +type opcode int + +// https://tools.ietf.org/html/rfc6455#section-11.8. +const ( + opContinuation opcode = iota + opText + opBinary + // 3 - 7 are reserved for further non-control frames. + _ + _ + _ + _ + _ + opClose + opPing + opPong + // 11-16 are reserved for further control frames. +) + // Conn provides a wrapper around the browser WebSocket API. type Conn struct { ws wsjs.WebSocket @@ -302,7 +322,7 @@ func (c *Conn) Reader(ctx context.Context) (MessageType, io.Reader, error) { // It buffers the entire message in memory and then sends it when the writer // is closed. func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - return writer{ + return &writer{ c: c, ctx: ctx, typ: typ, @@ -320,7 +340,7 @@ type writer struct { b *bytes.Buffer } -func (w writer) Write(p []byte) (int, error) { +func (w *writer) Write(p []byte) (int, error) { if w.closed { return 0, errors.New("cannot write to closed writer") } @@ -331,7 +351,7 @@ func (w writer) Write(p []byte) (int, error) { return n, nil } -func (w writer) Close() error { +func (w *writer) Close() error { if w.closed { return errors.New("cannot close closed writer") } From 3f26c9f6f1ec6ac0bba963f250194a55da16a211 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 00:57:27 -0700 Subject: [PATCH 052/152] wsjson: Write messages in a single frame always Closes #315 --- internal/util/util.go | 7 +++++++ wsjson/wsjson.go | 17 +++++++++-------- 2 files changed, 16 insertions(+), 8 deletions(-) create mode 100644 internal/util/util.go diff --git a/internal/util/util.go b/internal/util/util.go new file mode 100644 index 00000000..1ff25dac --- /dev/null +++ b/internal/util/util.go @@ -0,0 +1,7 @@ +package util + +type WriterFunc func(p []byte) (int, error) + +func (f WriterFunc) Write(p []byte) (int, error) { + return f(p) +} diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index 2000a77a..c6b29ee1 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -8,6 +8,7 @@ import ( "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" + "nhooyr.io/websocket/internal/util" "nhooyr.io/websocket/internal/errd" ) @@ -51,17 +52,17 @@ func Write(ctx context.Context, c *websocket.Conn, v interface{}) error { func write(ctx context.Context, c *websocket.Conn, v interface{}) (err error) { defer errd.Wrap(&err, "failed to write JSON message") - w, err := c.Writer(ctx, websocket.MessageText) - if err != nil { - return err - } - // json.Marshal cannot reuse buffers between calls as it has to return // a copy of the byte slice but Encoder does as it directly writes to w. - err = json.NewEncoder(w).Encode(v) + err = json.NewEncoder(util.WriterFunc(func(p []byte) (int, error) { + err := c.Write(ctx, websocket.MessageText, p) + if err != nil { + return 0, err + } + return len(p), nil + })).Encode(v) if err != nil { return fmt.Errorf("failed to marshal JSON: %w", err) } - - return w.Close() + return nil } From f7bed7c75ec38a4cee43efdbba57da76daf5305e Mon Sep 17 00:00:00 2001 From: Martin Benda Date: Thu, 28 Apr 2022 15:26:06 +0200 Subject: [PATCH 053/152] Extend DialOptions to allow Host header override --- dial.go | 7 ++++++ dial_test.go | 60 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/dial.go b/dial.go index 4b2b7b62..510b94b1 100644 --- a/dial.go +++ b/dial.go @@ -30,6 +30,10 @@ type DialOptions struct { // HTTPHeader specifies the HTTP headers included in the handshake request. HTTPHeader http.Header + // Host optionally overrides the Host HTTP header to send. If empty, the value + // of URL.Host will be used. + Host string + // Subprotocols lists the WebSocket subprotocols to negotiate with the server. Subprotocols []string @@ -168,6 +172,9 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts if err != nil { return nil, fmt.Errorf("failed to create new http request: %w", err) } + if len(opts.Host) > 0 { + req.Host = opts.Host + } req.Header = opts.HTTPHeader.Clone() req.Header.Set("Connection", "Upgrade") req.Header.Set("Upgrade", "websocket") diff --git a/dial_test.go b/dial_test.go index 75d59540..8680147e 100644 --- a/dial_test.go +++ b/dial_test.go @@ -4,6 +4,7 @@ package websocket import ( + "bytes" "context" "crypto/rand" "io" @@ -118,6 +119,65 @@ func TestBadDials(t *testing.T) { }) } +func Test_verifyHostOverride(t *testing.T) { + testCases := []struct { + name string + host string + exp string + }{ + { + name: "noOverride", + host: "", + exp: "example.com", + }, + { + name: "hostOverride", + host: "example.net", + exp: "example.net", + }, + } + + for _, tc := range testCases { + tc := tc + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + rt := func(r *http.Request) (*http.Response, error) { + assert.Equal(t, "Host", tc.exp, r.Host) + + h := http.Header{} + h.Set("Connection", "Upgrade") + h.Set("Upgrade", "websocket") + h.Set("Sec-WebSocket-Accept", secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) + + return &http.Response{ + StatusCode: http.StatusSwitchingProtocols, + Header: h, + Body: mockBody{bytes.NewBufferString("hi")}, + }, nil + } + + _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + HTTPClient: mockHTTPClient(rt), + Host: tc.host, + }) + assert.Success(t, err) + }) + } + +} + +type mockBody struct { + *bytes.Buffer +} + +func (mb mockBody) Close() error { + return nil +} + func Test_verifyServerHandshake(t *testing.T) { t.Parallel() From 98732747dc4b5e44bdbd80e6af21cde946621511 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 01:08:25 -0700 Subject: [PATCH 054/152] wsjson: fmt --- wsjson/wsjson.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/wsjson/wsjson.go b/wsjson/wsjson.go index c6b29ee1..7c986a0d 100644 --- a/wsjson/wsjson.go +++ b/wsjson/wsjson.go @@ -8,8 +8,8 @@ import ( "nhooyr.io/websocket" "nhooyr.io/websocket/internal/bpool" - "nhooyr.io/websocket/internal/util" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/util" ) // Read reads a JSON message from c into v. From fecf26c12678e046275c4e99fad7f9bcda78fd83 Mon Sep 17 00:00:00 2001 From: photostorm Date: Fri, 23 Apr 2021 23:20:27 -0400 Subject: [PATCH 055/152] netconn.go: Return real remote and local address where possible --- conn_test.go | 4 ++-- netconn.go | 17 +++++++---------- netconn_js.go | 11 +++++++++++ netconn_notjs.go | 20 ++++++++++++++++++++ 4 files changed, 40 insertions(+), 12 deletions(-) create mode 100644 netconn_js.go create mode 100644 netconn_notjs.go diff --git a/conn_test.go b/conn_test.go index a3f3d787..b9e2063d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -155,8 +155,8 @@ func TestConn(t *testing.T) { n1.SetDeadline(time.Time{}) assert.Equal(t, "remote addr", n1.RemoteAddr(), n1.LocalAddr()) - assert.Equal(t, "remote addr string", "websocket/unknown-addr", n1.RemoteAddr().String()) - assert.Equal(t, "remote addr network", "websocket", n1.RemoteAddr().Network()) + assert.Equal(t, "remote addr string", "pipe", n1.RemoteAddr().String()) + assert.Equal(t, "remote addr network", "pipe", n1.RemoteAddr().Network()) errs := xsync.Go(func() error { _, err := n2.Write([]byte("hello")) diff --git a/netconn.go b/netconn.go index 4af6c202..74000c9e 100644 --- a/netconn.go +++ b/netconn.go @@ -33,8 +33,13 @@ import ( // where only the reading/writing goroutines are interrupted but the connection // is kept alive. // -// The Addr methods will return a mock net.Addr that returns "websocket" for Network -// and "websocket/unknown-addr" for String. +// The Addr methods will return the real addresses for connections obtained +// from websocket.Accept. But for connections obtained from websocket.Dial, a mock net.Addr +// will be returned that gives "websocket" for Network() and "websocket/unknown-addr" for +// String(). This is because websocket.Dial only exposes a io.ReadWriteCloser instead of the +// full net.Conn to us. +// +// When running as WASM, the Addr methods will always return the mock address described above. // // A received StatusNormalClosure or StatusGoingAway close frame will be translated to // io.EOF when reading. @@ -181,14 +186,6 @@ func (a websocketAddr) String() string { return "websocket/unknown-addr" } -func (nc *netConn) RemoteAddr() net.Addr { - return websocketAddr{} -} - -func (nc *netConn) LocalAddr() net.Addr { - return websocketAddr{} -} - func (nc *netConn) SetDeadline(t time.Time) error { nc.SetWriteDeadline(t) nc.SetReadDeadline(t) diff --git a/netconn_js.go b/netconn_js.go new file mode 100644 index 00000000..ccc8c89f --- /dev/null +++ b/netconn_js.go @@ -0,0 +1,11 @@ +package websocket + +import "net" + +func (nc *netConn) RemoteAddr() net.Addr { + return websocketAddr{} +} + +func (nc *netConn) LocalAddr() net.Addr { + return websocketAddr{} +} diff --git a/netconn_notjs.go b/netconn_notjs.go new file mode 100644 index 00000000..f3eb0d66 --- /dev/null +++ b/netconn_notjs.go @@ -0,0 +1,20 @@ +//go:build !js +// +build !js + +package websocket + +import "net" + +func (nc *netConn) RemoteAddr() net.Addr { + if unc, ok := nc.c.rwc.(net.Conn); ok { + return unc.RemoteAddr() + } + return websocketAddr{} +} + +func (nc *netConn) LocalAddr() net.Addr { + if unc, ok := nc.c.rwc.(net.Conn); ok { + return unc.LocalAddr() + } + return websocketAddr{} +} From 5793e7d5804bf2bc775a271e5882625c02f83d47 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 01:23:08 -0700 Subject: [PATCH 056/152] internal/util: golint --- internal/util/util.go | 1 + 1 file changed, 1 insertion(+) diff --git a/internal/util/util.go b/internal/util/util.go index 1ff25dac..f23fb67b 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -1,5 +1,6 @@ package util +// WriterFunc is used to implement one off io.Writers. type WriterFunc func(p []byte) (int, error) func (f WriterFunc) Write(p []byte) (int, error) { From 2598ea2175350ae8280757752ac6143693506e6d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 02:07:24 -0700 Subject: [PATCH 057/152] Remove third party dependencies from go.mod and go.sum Closes #297 --- README.md | 4 +- ci/lint.sh | 23 ++++ ci/test.sh | 9 ++ conn_test.go | 42 +------- frame_test.go | 88 --------------- go.mod | 37 ------- go.sum | 95 ----------------- {examples => internal/examples}/README.md | 0 .../examples}/chat/README.md | 0 {examples => internal/examples}/chat/chat.go | 0 .../examples}/chat/chat_test.go | 0 .../examples}/chat/index.css | 0 .../examples}/chat/index.html | 0 {examples => internal/examples}/chat/index.js | 0 {examples => internal/examples}/chat/main.go | 0 .../examples}/echo/README.md | 0 {examples => internal/examples}/echo/main.go | 0 .../examples}/echo/server.go | 0 .../examples}/echo/server_test.go | 0 internal/examples/go.mod | 11 ++ internal/examples/go.sum | 41 +++++++ internal/test/assert/assert.go | 16 +-- internal/test/wstest/echo.go | 3 +- internal/thirdparty/doc.go | 2 + internal/thirdparty/frame_test.go | 100 ++++++++++++++++++ internal/thirdparty/gin_test.go | 75 +++++++++++++ internal/thirdparty/go.mod | 41 +++++++ internal/thirdparty/go.sum | 94 ++++++++++++++++ 28 files changed, 406 insertions(+), 275 deletions(-) rename {examples => internal/examples}/README.md (100%) rename {examples => internal/examples}/chat/README.md (100%) rename {examples => internal/examples}/chat/chat.go (100%) rename {examples => internal/examples}/chat/chat_test.go (100%) rename {examples => internal/examples}/chat/index.css (100%) rename {examples => internal/examples}/chat/index.html (100%) rename {examples => internal/examples}/chat/index.js (100%) rename {examples => internal/examples}/chat/main.go (100%) rename {examples => internal/examples}/echo/README.md (100%) rename {examples => internal/examples}/echo/main.go (100%) rename {examples => internal/examples}/echo/server.go (100%) rename {examples => internal/examples}/echo/server_test.go (100%) create mode 100644 internal/examples/go.mod create mode 100644 internal/examples/go.sum create mode 100644 internal/thirdparty/doc.go create mode 100644 internal/thirdparty/frame_test.go create mode 100644 internal/thirdparty/gin_test.go create mode 100644 internal/thirdparty/go.mod create mode 100644 internal/thirdparty/go.sum diff --git a/README.md b/README.md index 4e73a266..f1a45972 100644 --- a/README.md +++ b/README.md @@ -39,9 +39,9 @@ go get nhooyr.io/websocket ## Examples For a production quality example that demonstrates the complete API, see the -[echo example](./examples/echo). +[echo example](./internal/examples/echo). -For a full stack example, see the [chat example](./examples/chat). +For a full stack example, see the [chat example](./internal/examples/chat). ### Server diff --git a/ci/lint.sh b/ci/lint.sh index a8ab3027..80f309be 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -12,3 +12,26 @@ GOOS=js GOARCH=wasm golint -set_exit_status ./... go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck ./... GOOS=js GOARCH=wasm staticcheck ./... + +govulncheck() { + tmpf=$(mktemp) + if ! command govulncheck "$@" >"$tmpf" 2>&1; then + cat "$tmpf" + fi +} +go install golang.org/x/vuln/cmd/govulncheck@latest +govulncheck ./... +GOOS=js GOARCH=wasm govulncheck ./... + +( + cd ./internal/examples + go vet ./... + staticcheck ./... + govulncheck ./... +) +( + cd ./internal/thirdparty + go vet ./... + staticcheck ./... + govulncheck ./... +) diff --git a/ci/test.sh b/ci/test.sh index 1b3d6cc3..32bdcec1 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -12,3 +12,12 @@ sed -i.bak '/examples/d' ci/out/coverage.prof go tool cover -func ci/out/coverage.prof | tail -n1 go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html + +( + cd ./internal/examples + go test "$@" ./... +) +( + cd ./internal/thirdparty + go test "$@" ./... +) diff --git a/conn_test.go b/conn_test.go index b9e2063d..d80acce2 100644 --- a/conn_test.go +++ b/conn_test.go @@ -1,11 +1,11 @@ //go:build !js -// +build !js package websocket_test import ( "bytes" "context" + "errors" "fmt" "io" "net/http" @@ -16,8 +16,6 @@ import ( "testing" "time" - "github.com/gin-gonic/gin" - "nhooyr.io/websocket" "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/test/assert" @@ -140,7 +138,9 @@ func TestConn(t *testing.T) { defer cancel() err = c1.Write(ctx, websocket.MessageText, []byte("x")) - assert.Equal(t, "write error", context.DeadlineExceeded, err) + if !errors.Is(err, context.DeadlineExceeded) { + t.Fatalf("unexpected error: %#v", err) + } }) t.Run("netConn", func(t *testing.T) { @@ -482,37 +482,3 @@ func echoServer(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOp err = wstest.EchoLoop(r.Context(), c) return assertCloseStatus(websocket.StatusNormalClosure, err) } - -func TestGin(t *testing.T) { - t.Parallel() - - gin.SetMode(gin.ReleaseMode) - r := gin.New() - r.GET("/", func(ginCtx *gin.Context) { - err := echoServer(ginCtx.Writer, ginCtx.Request, nil) - if err != nil { - t.Error(err) - } - }) - - s := httptest.NewServer(r) - defer s.Close() - - ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) - defer cancel() - - c, _, err := websocket.Dial(ctx, s.URL, nil) - assert.Success(t, err) - defer c.Close(websocket.StatusInternalError, "") - - err = wsjson.Write(ctx, c, "hello") - assert.Success(t, err) - - var v interface{} - err = wsjson.Read(ctx, c, &v) - assert.Success(t, err) - assert.Equal(t, "read msg", "hello", v) - - err = c.Close(websocket.StatusNormalClosure, "") - assert.Success(t, err) -} diff --git a/frame_test.go b/frame_test.go index 2f4f2e25..e697e198 100644 --- a/frame_test.go +++ b/frame_test.go @@ -12,10 +12,6 @@ import ( "strconv" "testing" "time" - _ "unsafe" - - "github.com/gobwas/ws" - _ "github.com/gorilla/websocket" "nhooyr.io/websocket/internal/test/assert" ) @@ -109,87 +105,3 @@ func Test_mask(t *testing.T) { expKey32 := bits.RotateLeft32(key32, -8) assert.Equal(t, "key32", expKey32, gotKey32) } - -func basicMask(maskKey [4]byte, pos int, b []byte) int { - for i := range b { - b[i] ^= maskKey[pos&3] - pos++ - } - return pos & 3 -} - -//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes -func gorillaMaskBytes(key [4]byte, pos int, b []byte) int - -func Benchmark_mask(b *testing.B) { - sizes := []int{ - 2, - 3, - 4, - 8, - 16, - 32, - 128, - 512, - 4096, - 16384, - } - - fns := []struct { - name string - fn func(b *testing.B, key [4]byte, p []byte) - }{ - { - name: "basic", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - basicMask(key, 0, p) - } - }, - }, - - { - name: "nhooyr", - fn: func(b *testing.B, key [4]byte, p []byte) { - key32 := binary.LittleEndian.Uint32(key[:]) - b.ResetTimer() - - for i := 0; i < b.N; i++ { - mask(key32, p) - } - }, - }, - { - name: "gorilla", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - gorillaMaskBytes(key, 0, p) - } - }, - }, - { - name: "gobwas", - fn: func(b *testing.B, key [4]byte, p []byte) { - for i := 0; i < b.N; i++ { - ws.Cipher(p, key, 0) - } - }, - }, - } - - key := [4]byte{1, 2, 3, 4} - - for _, size := range sizes { - p := make([]byte, size) - - b.Run(strconv.Itoa(size), func(b *testing.B) { - for _, fn := range fns { - b.Run(fn.name, func(b *testing.B) { - b.SetBytes(int64(size)) - - fn.fn(b, key, p) - }) - } - }) - } -} diff --git a/go.mod b/go.mod index 95a1df92..715a9f7a 100644 --- a/go.mod +++ b/go.mod @@ -1,40 +1,3 @@ module nhooyr.io/websocket go 1.19 - -require ( - github.com/gin-gonic/gin v1.9.1 - github.com/gobwas/ws v1.3.0 - github.com/google/go-cmp v0.5.9 - github.com/gorilla/websocket v1.5.0 - golang.org/x/time v0.3.0 -) - -require ( - github.com/bytedance/sonic v1.9.1 // indirect - github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect - github.com/gabriel-vasile/mimetype v1.4.2 // indirect - github.com/gin-contrib/sse v0.1.0 // indirect - github.com/go-playground/locales v0.14.1 // indirect - github.com/go-playground/universal-translator v0.18.1 // indirect - github.com/go-playground/validator/v10 v10.14.0 // indirect - github.com/gobwas/httphead v0.1.0 // indirect - github.com/gobwas/pool v0.2.1 // indirect - github.com/goccy/go-json v0.10.2 // indirect - github.com/json-iterator/go v1.1.12 // indirect - github.com/klauspost/cpuid/v2 v2.2.4 // indirect - github.com/leodido/go-urn v1.2.4 // indirect - github.com/mattn/go-isatty v0.0.19 // indirect - github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect - github.com/modern-go/reflect2 v1.0.2 // indirect - github.com/pelletier/go-toml/v2 v2.0.8 // indirect - github.com/twitchyliquid64/golang-asm v0.15.1 // indirect - github.com/ugorji/go/codec v1.2.11 // indirect - golang.org/x/arch v0.3.0 // indirect - golang.org/x/crypto v0.9.0 // indirect - golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect - golang.org/x/text v0.9.0 // indirect - google.golang.org/protobuf v1.30.0 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum index dc4743dd..e69de29b 100644 --- a/go.sum +++ b/go.sum @@ -1,95 +0,0 @@ -github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= -github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= -github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= -github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= -github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= -github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= -github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= -github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= -github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= -github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= -github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= -github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= -github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= -github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= -github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= -github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= -github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= -github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= -github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= -github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= -github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= -github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= -github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= -github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= -github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= -github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= -github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= -github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= -github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= -github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= -github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= -github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= -github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= -github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= -github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= -github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= -golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= -golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= -golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= -golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= -golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= -google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= diff --git a/examples/README.md b/internal/examples/README.md similarity index 100% rename from examples/README.md rename to internal/examples/README.md diff --git a/examples/chat/README.md b/internal/examples/chat/README.md similarity index 100% rename from examples/chat/README.md rename to internal/examples/chat/README.md diff --git a/examples/chat/chat.go b/internal/examples/chat/chat.go similarity index 100% rename from examples/chat/chat.go rename to internal/examples/chat/chat.go diff --git a/examples/chat/chat_test.go b/internal/examples/chat/chat_test.go similarity index 100% rename from examples/chat/chat_test.go rename to internal/examples/chat/chat_test.go diff --git a/examples/chat/index.css b/internal/examples/chat/index.css similarity index 100% rename from examples/chat/index.css rename to internal/examples/chat/index.css diff --git a/examples/chat/index.html b/internal/examples/chat/index.html similarity index 100% rename from examples/chat/index.html rename to internal/examples/chat/index.html diff --git a/examples/chat/index.js b/internal/examples/chat/index.js similarity index 100% rename from examples/chat/index.js rename to internal/examples/chat/index.js diff --git a/examples/chat/main.go b/internal/examples/chat/main.go similarity index 100% rename from examples/chat/main.go rename to internal/examples/chat/main.go diff --git a/examples/echo/README.md b/internal/examples/echo/README.md similarity index 100% rename from examples/echo/README.md rename to internal/examples/echo/README.md diff --git a/examples/echo/main.go b/internal/examples/echo/main.go similarity index 100% rename from examples/echo/main.go rename to internal/examples/echo/main.go diff --git a/examples/echo/server.go b/internal/examples/echo/server.go similarity index 100% rename from examples/echo/server.go rename to internal/examples/echo/server.go diff --git a/examples/echo/server_test.go b/internal/examples/echo/server_test.go similarity index 100% rename from examples/echo/server_test.go rename to internal/examples/echo/server_test.go diff --git a/internal/examples/go.mod b/internal/examples/go.mod new file mode 100644 index 00000000..ef4c5f67 --- /dev/null +++ b/internal/examples/go.mod @@ -0,0 +1,11 @@ +module nhooyr.io/websocket/examples + +go 1.22 + +replace nhooyr.io/websocket => ../.. + +require ( + github.com/klauspost/compress v1.10.3 // indirect + golang.org/x/time v0.3.0 // indirect + nhooyr.io/websocket v1.8.7 // indirect +) diff --git a/internal/examples/go.sum b/internal/examples/go.sum new file mode 100644 index 00000000..03aa32c2 --- /dev/null +++ b/internal/examples/go.sum @@ -0,0 +1,41 @@ +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= +github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= +github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= +golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index e37e9573..64c938c5 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -5,24 +5,14 @@ import ( "reflect" "strings" "testing" - - "github.com/google/go-cmp/cmp" - "github.com/google/go-cmp/cmp/cmpopts" ) -// Diff returns a human readable diff between v1 and v2 -func Diff(v1, v2 interface{}) string { - return cmp.Diff(v1, v2, cmpopts.EquateErrors(), cmp.Exporter(func(r reflect.Type) bool { - return true - })) -} - // Equal asserts exp == act. -func Equal(t testing.TB, name string, exp, act interface{}) { +func Equal(t testing.TB, name string, exp, got interface{}) { t.Helper() - if diff := Diff(exp, act); diff != "" { - t.Fatalf("unexpected %v: %v", name, diff) + if !reflect.DeepEqual(exp, got) { + t.Fatalf("unexpected %v: expected %#v but got %#v", name, exp, got) } } diff --git a/internal/test/wstest/echo.go b/internal/test/wstest/echo.go index 0938a138..dc21a8f0 100644 --- a/internal/test/wstest/echo.go +++ b/internal/test/wstest/echo.go @@ -8,7 +8,6 @@ import ( "time" "nhooyr.io/websocket" - "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/xrand" "nhooyr.io/websocket/internal/xsync" ) @@ -76,7 +75,7 @@ func Echo(ctx context.Context, c *websocket.Conn, max int) error { } if !bytes.Equal(msg, act) { - return fmt.Errorf("unexpected msg read: %v", assert.Diff(msg, act)) + return fmt.Errorf("unexpected msg read: %#v", act) } return nil diff --git a/internal/thirdparty/doc.go b/internal/thirdparty/doc.go new file mode 100644 index 00000000..e756d09f --- /dev/null +++ b/internal/thirdparty/doc.go @@ -0,0 +1,2 @@ +// Package thirdparty contains third party benchmarks and tests. +package thirdparty diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go new file mode 100644 index 00000000..1a0ed125 --- /dev/null +++ b/internal/thirdparty/frame_test.go @@ -0,0 +1,100 @@ +package thirdparty + +import ( + "encoding/binary" + "strconv" + "testing" + _ "unsafe" + + "github.com/gobwas/ws" + _ "github.com/gorilla/websocket" + + _ "nhooyr.io/websocket" +) + +func basicMask(maskKey [4]byte, pos int, b []byte) int { + for i := range b { + b[i] ^= maskKey[pos&3] + pos++ + } + return pos & 3 +} + +//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes +func gorillaMaskBytes(key [4]byte, pos int, b []byte) int + +//go:linkname mask nhooyr.io/websocket.mask +func mask(key32 uint32, b []byte) int + +func Benchmark_mask(b *testing.B) { + sizes := []int{ + 2, + 3, + 4, + 8, + 16, + 32, + 128, + 512, + 4096, + 16384, + } + + fns := []struct { + name string + fn func(b *testing.B, key [4]byte, p []byte) + }{ + { + name: "basic", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + basicMask(key, 0, p) + } + }, + }, + + { + name: "nhooyr", + fn: func(b *testing.B, key [4]byte, p []byte) { + key32 := binary.LittleEndian.Uint32(key[:]) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + mask(key32, p) + } + }, + }, + { + name: "gorilla", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + gorillaMaskBytes(key, 0, p) + } + }, + }, + { + name: "gobwas", + fn: func(b *testing.B, key [4]byte, p []byte) { + for i := 0; i < b.N; i++ { + ws.Cipher(p, key, 0) + } + }, + }, + } + + key := [4]byte{1, 2, 3, 4} + + for _, size := range sizes { + p := make([]byte, size) + + b.Run(strconv.Itoa(size), func(b *testing.B) { + for _, fn := range fns { + b.Run(fn.name, func(b *testing.B) { + b.SetBytes(int64(size)) + + fn.fn(b, key, p) + }) + } + }) + } +} diff --git a/internal/thirdparty/gin_test.go b/internal/thirdparty/gin_test.go new file mode 100644 index 00000000..6d59578d --- /dev/null +++ b/internal/thirdparty/gin_test.go @@ -0,0 +1,75 @@ +package thirdparty + +import ( + "context" + "fmt" + "net/http" + "net/http/httptest" + "testing" + "time" + + "github.com/gin-gonic/gin" + + "nhooyr.io/websocket" + "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/test/assert" + "nhooyr.io/websocket/internal/test/wstest" + "nhooyr.io/websocket/wsjson" +) + +func TestGin(t *testing.T) { + t.Parallel() + + gin.SetMode(gin.ReleaseMode) + r := gin.New() + r.GET("/", func(ginCtx *gin.Context) { + err := echoServer(ginCtx.Writer, ginCtx.Request, nil) + if err != nil { + t.Error(err) + } + }) + + s := httptest.NewServer(r) + defer s.Close() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*30) + defer cancel() + + c, _, err := websocket.Dial(ctx, s.URL, nil) + assert.Success(t, err) + defer c.Close(websocket.StatusInternalError, "") + + err = wsjson.Write(ctx, c, "hello") + assert.Success(t, err) + + var v interface{} + err = wsjson.Read(ctx, c, &v) + assert.Success(t, err) + assert.Equal(t, "read msg", "hello", v) + + err = c.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) +} + +func echoServer(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOptions) (err error) { + defer errd.Wrap(&err, "echo server failed") + + c, err := websocket.Accept(w, r, opts) + if err != nil { + return err + } + defer c.Close(websocket.StatusInternalError, "") + + err = wstest.EchoLoop(r.Context(), c) + return assertCloseStatus(websocket.StatusNormalClosure, err) +} + +func assertCloseStatus(exp websocket.StatusCode, err error) error { + if websocket.CloseStatus(err) == -1 { + return fmt.Errorf("expected websocket.CloseError: %T %v", err, err) + } + if websocket.CloseStatus(err) != exp { + return fmt.Errorf("expected close status %v but got %v", exp, err) + } + return nil +} diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod new file mode 100644 index 00000000..b0a979f5 --- /dev/null +++ b/internal/thirdparty/go.mod @@ -0,0 +1,41 @@ +module nhooyr.io/websocket/internal/thirdparty + +go 1.22 + +replace nhooyr.io/websocket => ../.. + +require ( + github.com/gin-gonic/gin v1.9.1 + github.com/gobwas/ws v1.3.0 + github.com/gorilla/websocket v1.5.0 + nhooyr.io/websocket v1.8.7 +) + +require ( + github.com/bytedance/sonic v1.9.1 // indirect + github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect + github.com/gabriel-vasile/mimetype v1.4.2 // indirect + github.com/gin-contrib/sse v0.1.0 // indirect + github.com/go-playground/locales v0.14.1 // indirect + github.com/go-playground/universal-translator v0.18.1 // indirect + github.com/go-playground/validator/v10 v10.14.0 // indirect + github.com/gobwas/httphead v0.1.0 // indirect + github.com/gobwas/pool v0.2.1 // indirect + github.com/goccy/go-json v0.10.2 // indirect + github.com/json-iterator/go v1.1.12 // indirect + github.com/klauspost/cpuid/v2 v2.2.4 // indirect + github.com/leodido/go-urn v1.2.4 // indirect + github.com/mattn/go-isatty v0.0.19 // indirect + github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect + github.com/modern-go/reflect2 v1.0.2 // indirect + github.com/pelletier/go-toml/v2 v2.0.8 // indirect + github.com/twitchyliquid64/golang-asm v0.15.1 // indirect + github.com/ugorji/go/codec v1.2.11 // indirect + golang.org/x/arch v0.3.0 // indirect + golang.org/x/crypto v0.9.0 // indirect + golang.org/x/net v0.10.0 // indirect + golang.org/x/sys v0.8.0 // indirect + golang.org/x/text v0.9.0 // indirect + google.golang.org/protobuf v1.30.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum new file mode 100644 index 00000000..80e4ad52 --- /dev/null +++ b/internal/thirdparty/go.sum @@ -0,0 +1,94 @@ +github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= +github.com/bytedance/sonic v1.9.1 h1:6iJ6NqdoxCDr6mbY8h18oSO+cShGSMRGCEo7F2h0x8s= +github.com/bytedance/sonic v1.9.1/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U= +github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams= +github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gabriel-vasile/mimetype v1.4.2 h1:w5qFW6JKBz9Y393Y4q372O9A7cUSequkh1Q7OhCmWKU= +github.com/gabriel-vasile/mimetype v1.4.2/go.mod h1:zApsH/mKG4w07erKIaJPFiX0Tsq9BFQgN3qGY5GnNgA= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= +github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= +github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= +github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= +github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= +github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= +github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= +github.com/go-playground/validator/v10 v10.14.0 h1:vgvQWe3XCz3gIeFDm/HnTIbj6UGmg/+t63MyGU2n5js= +github.com/go-playground/validator/v10 v10.14.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= +github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU= +github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM= +github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og= +github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.3.0 h1:sbeU3Y4Qzlb+MOzIe6mQGf7QR4Hkv6ZD0qhGkBFL2O0= +github.com/gobwas/ws v1.3.0/go.mod h1:hRKAFb8wOxFROYNsT1bqfWnhX+b5MFeJM9r2ZSwg/KY= +github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= +github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= +github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= +github.com/google/go-cmp v0.5.5 h1:Khx7svrCpmxxtHBq5j2mp/xVjsi8hQMfNLvJFAlrGgU= +github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= +github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= +github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= +github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= +github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= +github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= +github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= +github.com/pelletier/go-toml/v2 v2.0.8 h1:0ctb6s9mE31h0/lhu+J6OPmVeDxJn+kYnJc2jZR9tGQ= +github.com/pelletier/go-toml/v2 v2.0.8/go.mod h1:vuYfssBdrU2XDZ9bYydBu6t+6a6PYNcZljzZR9VXg+4= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= +github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= +github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.3 h1:RP3t2pwF7cMEbC1dqtB6poj3niw/9gnV4Cjg5oW5gtY= +github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= +github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= +github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= +github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= +golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= +golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= +golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= +golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= +golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= +google.golang.org/protobuf v1.30.0 h1:kPPoIgf3TsEvrm0PFe15JQ+570QVxYzEvvHqChK+cng= +google.golang.org/protobuf v1.30.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= From b4b86b904ee818dc480b8b7384bd92a751a5c0ee Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 02:17:45 -0700 Subject: [PATCH 058/152] dial.go: Use timeout on HTTPClient properly Closes #341 --- conn_test.go | 31 +++++++++++++++++++++++++++++++ dial.go | 9 +++++---- 2 files changed, 36 insertions(+), 4 deletions(-) diff --git a/conn_test.go b/conn_test.go index d80acce2..59661b73 100644 --- a/conn_test.go +++ b/conn_test.go @@ -264,6 +264,37 @@ func TestConn(t *testing.T) { err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) }) + + t.Run("HTTPClient.Timeout", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, &websocket.DialOptions{ + HTTPClient: &http.Client{Timeout: time.Second*5}, + }, nil) + + tt.goEchoLoop(c2) + + c1.SetReadLimit(1 << 30) + + exp := xrand.String(xrand.Int(131072)) + + werr := xsync.Go(func() error { + return wsjson.Write(tt.ctx, c1, exp) + }) + + var act interface{} + err := wsjson.Read(tt.ctx, c1, &act) + assert.Success(t, err) + assert.Equal(t, "read msg", exp, act) + + select { + case err := <-werr: + assert.Success(t, err) + case <-tt.ctx.Done(): + t.Fatal(tt.ctx.Err()) + } + + err = c1.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + }) } func TestWasm(t *testing.T) { diff --git a/dial.go b/dial.go index 510b94b1..0f2735da 100644 --- a/dial.go +++ b/dial.go @@ -59,12 +59,13 @@ func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context } if o.HTTPClient == nil { o.HTTPClient = http.DefaultClient - } else if opts.HTTPClient.Timeout > 0 { - ctx, cancel = context.WithTimeout(ctx, opts.HTTPClient.Timeout) + } + if o.HTTPClient.Timeout > 0 { + ctx, cancel = context.WithTimeout(ctx, o.HTTPClient.Timeout) - newClient := *opts.HTTPClient + newClient := *o.HTTPClient newClient.Timeout = 0 - opts.HTTPClient = &newClient + o.HTTPClient = &newClient } if o.HTTPHeader == nil { o.HTTPHeader = http.Header{} From a6b946487cbd40aaa9867930235c1d2ed7017f53 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 02:23:30 -0700 Subject: [PATCH 059/152] conn: Add noCopy Closes #349 --- conn.go | 5 +++++ ws_js.go | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/conn.go b/conn.go index ab37248e..17a6b966 100644 --- a/conn.go +++ b/conn.go @@ -42,6 +42,8 @@ const ( // This applies to context expirations as well unfortunately. // See https://github.com/nhooyr/websocket/issues/242#issuecomment-633182220 type Conn struct { + noCopy + subprotocol string rwc io.ReadWriteCloser client bool @@ -288,3 +290,6 @@ func (m *mu) unlock() { default: } } + +type noCopy struct{} +func (*noCopy) Lock() {} diff --git a/ws_js.go b/ws_js.go index 3248933c..05f2202e 100644 --- a/ws_js.go +++ b/ws_js.go @@ -40,6 +40,7 @@ const ( // Conn provides a wrapper around the browser WebSocket API. type Conn struct { + noCopy ws wsjs.WebSocket // read limit for a message in bytes. @@ -563,3 +564,6 @@ func (m *mu) unlock() { default: } } + +type noCopy struct{} +func (*noCopy) Lock() {} From 4e15d756f556869a9f170f7b52ac357e9b6ae888 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 02:32:37 -0700 Subject: [PATCH 060/152] ci/bench.sh: Add --- .github/workflows/daily.yml | 10 +++++++++- ci/bench.sh | 9 +++++++++ make.sh | 1 + 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100755 ci/bench.sh diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index cbac574d..d10c142f 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -8,7 +8,15 @@ concurrency: cancel-in-progress: true jobs: - ci: + bench: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: AUTOBAHN=1 ./ci/bench.sh + test: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/ci/bench.sh b/ci/bench.sh new file mode 100755 index 00000000..31bf2f15 --- /dev/null +++ b/ci/bench.sh @@ -0,0 +1,9 @@ +#!/bin/sh +set -eu +cd -- "$(dirname "$0")/.." + +go test --bench=. "$@" ./... +( + cd ./internal/thirdparty + go test --bench=. "$@" ./... +) diff --git a/make.sh b/make.sh index 6f5d1f57..68a98ac1 100755 --- a/make.sh +++ b/make.sh @@ -5,3 +5,4 @@ cd -- "$(dirname "$0")" ./ci/fmt.sh ./ci/lint.sh ./ci/test.sh +./ci/bench.sh From a02cbef5605d23c97972fbea8dd16488cf437b7a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 03:34:15 -0700 Subject: [PATCH 061/152] compress.go: Fix context takeover --- accept.go | 1 + ci/bench.sh | 4 ++-- compress.go | 16 ++++++---------- conn.go | 1 + conn_test.go | 4 ++-- dial_test.go | 3 ++- export_test.go | 6 ++++-- internal/util/util.go | 7 +++++++ internal/xsync/go.go | 3 ++- read.go | 27 ++++++++++++++++----------- write.go | 11 +++-------- ws_js.go | 1 + 12 files changed, 47 insertions(+), 37 deletions(-) diff --git a/accept.go b/accept.go index ff2033e7..6c63e730 100644 --- a/accept.go +++ b/accept.go @@ -269,6 +269,7 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi if strings.HasPrefix(p, "client_max_window_bits") { // We cannot adjust the read sliding window so cannot make use of this. + // By not responding to it, we tell the client we're ignoring it. continue } diff --git a/ci/bench.sh b/ci/bench.sh index 31bf2f15..8f99278d 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -2,8 +2,8 @@ set -eu cd -- "$(dirname "$0")/.." -go test --bench=. "$@" ./... +go test --run=^$ --bench=. "$@" ./... ( cd ./internal/thirdparty - go test --bench=. "$@" ./... + go test --run=^$ --bench=. "$@" ./... ) diff --git a/compress.go b/compress.go index e6722fc7..61e6e268 100644 --- a/compress.go +++ b/compress.go @@ -31,7 +31,7 @@ const ( CompressionDisabled CompressionMode = iota // CompressionContextTakeover uses a 32 kB sliding window and flate.Writer per connection. - // It reusing the sliding window from previous messages. + // It reuses the sliding window from previous messages. // As most WebSocket protocols are repetitive, this can be very efficient. // It carries an overhead of 32 kB + 1.2 MB for every connection compared to CompressionNoContextTakeover. // @@ -80,7 +80,7 @@ func (copts *compressionOptions) setHeader(h http.Header) { // They are removed when sending to avoid the overhead as // WebSocket framing tell's when the message has ended but then // we need to add them back otherwise flate.Reader keeps -// trying to return more bytes. +// trying to read more bytes. const deflateMessageTail = "\x00\x00\xff\xff" type trimLastFourBytesWriter struct { @@ -201,23 +201,19 @@ func (sw *slidingWindow) init(n int) { } p := slidingWindowPool(n) - buf, ok := p.Get().(*[]byte) + sw2, ok := p.Get().(*slidingWindow) if ok { - sw.buf = (*buf)[:0] + *sw = *sw2 } else { sw.buf = make([]byte, 0, n) } } func (sw *slidingWindow) close() { - if sw.buf == nil { - return - } - + sw.buf = sw.buf[:0] swPoolMu.Lock() - swPool[cap(sw.buf)].Put(&sw.buf) + swPool[cap(sw.buf)].Put(sw) swPoolMu.Unlock() - sw.buf = nil } func (sw *slidingWindow) write(p []byte) { diff --git a/conn.go b/conn.go index 17a6b966..81a57c7f 100644 --- a/conn.go +++ b/conn.go @@ -292,4 +292,5 @@ func (m *mu) unlock() { } type noCopy struct{} + func (*noCopy) Lock() {} diff --git a/conn_test.go b/conn_test.go index 59661b73..7a6a0c39 100644 --- a/conn_test.go +++ b/conn_test.go @@ -267,7 +267,7 @@ func TestConn(t *testing.T) { t.Run("HTTPClient.Timeout", func(t *testing.T) { tt, c1, c2 := newConnTest(t, &websocket.DialOptions{ - HTTPClient: &http.Client{Timeout: time.Second*5}, + HTTPClient: &http.Client{Timeout: time.Second * 5}, }, nil) tt.goEchoLoop(c2) @@ -458,7 +458,7 @@ func BenchmarkConn(b *testing.B) { typ, r, err := c1.Reader(bb.ctx) if err != nil { - b.Fatal(err) + b.Fatal(i, err) } if websocket.MessageText != typ { assert.Equal(b, "data type", websocket.MessageText, typ) diff --git a/dial_test.go b/dial_test.go index 8680147e..e072db2d 100644 --- a/dial_test.go +++ b/dial_test.go @@ -15,6 +15,7 @@ import ( "time" "nhooyr.io/websocket/internal/test/assert" + "nhooyr.io/websocket/internal/util" ) func TestBadDials(t *testing.T) { @@ -27,7 +28,7 @@ func TestBadDials(t *testing.T) { name string url string opts *DialOptions - rand readerFunc + rand util.ReaderFunc nilCtx bool }{ { diff --git a/export_test.go b/export_test.go index d618a154..8731b6d8 100644 --- a/export_test.go +++ b/export_test.go @@ -3,9 +3,11 @@ package websocket +import "nhooyr.io/websocket/internal/util" + func (c *Conn) RecordBytesWritten() *int { var bytesWritten int - c.bw.Reset(writerFunc(func(p []byte) (int, error) { + c.bw.Reset(util.WriterFunc(func(p []byte) (int, error) { bytesWritten += len(p) return c.rwc.Write(p) })) @@ -14,7 +16,7 @@ func (c *Conn) RecordBytesWritten() *int { func (c *Conn) RecordBytesRead() *int { var bytesRead int - c.br.Reset(readerFunc(func(p []byte) (int, error) { + c.br.Reset(util.ReaderFunc(func(p []byte) (int, error) { n, err := c.rwc.Read(p) bytesRead += n return n, err diff --git a/internal/util/util.go b/internal/util/util.go index f23fb67b..aa210703 100644 --- a/internal/util/util.go +++ b/internal/util/util.go @@ -6,3 +6,10 @@ type WriterFunc func(p []byte) (int, error) func (f WriterFunc) Write(p []byte) (int, error) { return f(p) } + +// ReaderFunc is used to implement one off io.Readers. +type ReaderFunc func(p []byte) (int, error) + +func (f ReaderFunc) Read(p []byte) (int, error) { + return f(p) +} diff --git a/internal/xsync/go.go b/internal/xsync/go.go index 7a61f27f..5229b12a 100644 --- a/internal/xsync/go.go +++ b/internal/xsync/go.go @@ -2,6 +2,7 @@ package xsync import ( "fmt" + "runtime/debug" ) // Go allows running a function in another goroutine @@ -13,7 +14,7 @@ func Go(fn func() error) <-chan error { r := recover() if r != nil { select { - case errs <- fmt.Errorf("panic in go fn: %v", r): + case errs <- fmt.Errorf("panic in go fn: %v, %s", r, debug.Stack()): default: } } diff --git a/read.go b/read.go index 7bc6f20d..d3217861 100644 --- a/read.go +++ b/read.go @@ -13,6 +13,7 @@ import ( "time" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/util" "nhooyr.io/websocket/internal/xsync" ) @@ -101,13 +102,20 @@ func newMsgReader(c *Conn) *msgReader { func (mr *msgReader) resetFlate() { if mr.flateContextTakeover() { + if mr.dict == nil { + mr.dict = &slidingWindow{} + } mr.dict.init(32768) } if mr.flateBufio == nil { mr.flateBufio = getBufioReader(mr.readFunc) } - mr.flateReader = getFlateReader(mr.flateBufio, mr.dict.buf) + if mr.flateContextTakeover() { + mr.flateReader = getFlateReader(mr.flateBufio, mr.dict.buf) + } else { + mr.flateReader = getFlateReader(mr.flateBufio, nil) + } mr.limitReader.r = mr.flateReader mr.flateTail.Reset(deflateMessageTail) } @@ -122,7 +130,10 @@ func (mr *msgReader) putFlateReader() { func (mr *msgReader) close() { mr.c.readMu.forceLock() mr.putFlateReader() - mr.dict.close() + if mr.dict != nil { + mr.dict.close() + mr.dict = nil + } if mr.flateBufio != nil { putBufioReader(mr.flateBufio) } @@ -348,14 +359,14 @@ type msgReader struct { flateBufio *bufio.Reader flateTail strings.Reader limitReader *limitReader - dict slidingWindow + dict *slidingWindow fin bool payloadLength int64 maskKey uint32 - // readerFunc(mr.Read) to avoid continuous allocations. - readFunc readerFunc + // util.ReaderFunc(mr.Read) to avoid continuous allocations. + readFunc util.ReaderFunc } func (mr *msgReader) reset(ctx context.Context, h header) { @@ -484,9 +495,3 @@ func (lr *limitReader) Read(p []byte) (int, error) { } return n, err } - -type readerFunc func(p []byte) (int, error) - -func (f readerFunc) Read(p []byte) (int, error) { - return f(p) -} diff --git a/write.go b/write.go index 7921eac9..500609dd 100644 --- a/write.go +++ b/write.go @@ -16,6 +16,7 @@ import ( "compress/flate" "nhooyr.io/websocket/internal/errd" + "nhooyr.io/websocket/internal/util" ) // Writer returns a writer bounded by the context that will write @@ -93,7 +94,7 @@ func newMsgWriterState(c *Conn) *msgWriterState { func (mw *msgWriterState) ensureFlate() { if mw.trimWriter == nil { mw.trimWriter = &trimLastFourBytesWriter{ - w: writerFunc(mw.write), + w: util.WriterFunc(mw.write), } } @@ -380,17 +381,11 @@ func (c *Conn) writeFramePayload(p []byte) (n int, err error) { return n, nil } -type writerFunc func(p []byte) (int, error) - -func (f writerFunc) Write(p []byte) (int, error) { - return f(p) -} - // extractBufioWriterBuf grabs the []byte backing a *bufio.Writer // and returns it. func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte { var writeBuf []byte - bw.Reset(writerFunc(func(p2 []byte) (int, error) { + bw.Reset(util.WriterFunc(func(p2 []byte) (int, error) { writeBuf = p2[:cap(p2)] return len(p2), nil })) diff --git a/ws_js.go b/ws_js.go index 05f2202e..9f0e19e9 100644 --- a/ws_js.go +++ b/ws_js.go @@ -566,4 +566,5 @@ func (m *mu) unlock() { } type noCopy struct{} + func (*noCopy) Lock() {} From 81afa8a34970dc1f5b2a59084a17d1a1a8d248ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 04:30:08 -0700 Subject: [PATCH 062/152] netconn: Avoid returning 0, nil in NetConn.Read Closes #367 --- netconn.go | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/netconn.go b/netconn.go index 74000c9e..e398b4f7 100644 --- a/netconn.go +++ b/netconn.go @@ -141,6 +141,19 @@ func (nc *netConn) Read(p []byte) (int, error) { nc.readMu.forceLock() defer nc.readMu.unlock() + for { + n, err := nc.read(p) + if err != nil { + return n, err + } + if n == 0 { + continue + } + return n, nil + } +} + +func (nc *netConn) read(p []byte) (int, error) { if atomic.LoadInt64(&nc.readExpired) == 1 { return 0, fmt.Errorf("failed to read: %w", context.DeadlineExceeded) } From 136f95448245daf0643ce6524382ccf80264d36e Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Tue, 8 Sep 2020 17:22:22 -0700 Subject: [PATCH 063/152] Client allows server to specify server_max_window_bits --- dial.go | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dial.go b/dial.go index 0f2735da..9acca133 100644 --- a/dial.go +++ b/dial.go @@ -273,6 +273,10 @@ func verifyServerExtensions(copts *compressionOptions, h http.Header) (*compress copts.serverNoContextTakeover = true continue } + if strings.HasPrefix(p, "server_max_window_bits=") { + // We can't adjust the deflate window, but decoding with a larger window is acceptable. + continue + } return nil, fmt.Errorf("unsupported permessage-deflate parameter: %q", p) } From 2291d83f761e83e3cb3946529d59f38309212a16 Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Tue, 8 Sep 2020 17:14:43 -0700 Subject: [PATCH 064/152] Server allows client to specify server_max_window_bits=15 --- accept.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/accept.go b/accept.go index 6c63e730..15e14285 100644 --- a/accept.go +++ b/accept.go @@ -265,6 +265,8 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi case "server_no_context_takeover": copts.serverNoContextTakeover = true continue + case "server_max_window_bits=15": + continue } if strings.HasPrefix(p, "client_max_window_bits") { From 711aa3f7aa251ac5628122bf0871cd59a32e9ce5 Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Tue, 8 Sep 2020 19:29:18 -0700 Subject: [PATCH 065/152] Server selects first acceptable compression offer Unacceptable offers are declined without rejecting the request. --- accept.go | 41 ++++++------- accept_test.go | 109 +++++++++++++++++++++------------ compress.go | 5 +- dial.go | 2 +- internal/test/assert/assert.go | 10 +++ 5 files changed, 102 insertions(+), 65 deletions(-) diff --git a/accept.go b/accept.go index 15e14285..19e388ec 100644 --- a/accept.go +++ b/accept.go @@ -123,9 +123,9 @@ func accept(w http.ResponseWriter, r *http.Request, opts *AcceptOptions) (_ *Con w.Header().Set("Sec-WebSocket-Protocol", subproto) } - copts, err := acceptCompression(r, w, opts.CompressionMode) - if err != nil { - return nil, err + copts, ok := selectDeflate(websocketExtensions(r.Header), opts.CompressionMode) + if ok { + w.Header().Set("Sec-WebSocket-Extensions", copts.String()) } w.WriteHeader(http.StatusSwitchingProtocols) @@ -238,25 +238,26 @@ func selectSubprotocol(r *http.Request, subprotocols []string) string { return "" } -func acceptCompression(r *http.Request, w http.ResponseWriter, mode CompressionMode) (*compressionOptions, error) { +func selectDeflate(extensions []websocketExtension, mode CompressionMode) (*compressionOptions, bool) { if mode == CompressionDisabled { - return nil, nil + return nil, false } - - for _, ext := range websocketExtensions(r.Header) { + for _, ext := range extensions { switch ext.name { // We used to implement x-webkit-deflate-fram too but Safari has bugs. // See https://github.com/nhooyr/websocket/issues/218 case "permessage-deflate": - return acceptDeflate(w, ext, mode) + copts, ok := acceptDeflate(ext, mode) + if ok { + return copts, true + } } } - return nil, nil + return nil, false } -func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode CompressionMode) (*compressionOptions, error) { +func acceptDeflate(ext websocketExtension, mode CompressionMode) (*compressionOptions, bool) { copts := mode.opts() - for _, p := range ext.params { switch p { case "client_no_context_takeover": @@ -265,24 +266,18 @@ func acceptDeflate(w http.ResponseWriter, ext websocketExtension, mode Compressi case "server_no_context_takeover": copts.serverNoContextTakeover = true continue - case "server_max_window_bits=15": + case "client_max_window_bits", + "server_max_window_bits=15": continue } - if strings.HasPrefix(p, "client_max_window_bits") { - // We cannot adjust the read sliding window so cannot make use of this. - // By not responding to it, we tell the client we're ignoring it. + if strings.HasPrefix(p, "client_max_window_bits=") { + // We can't adjust the deflate window, but decoding with a larger window is acceptable. continue } - - err := fmt.Errorf("unsupported permessage-deflate parameter: %q", p) - http.Error(w, err.Error(), http.StatusBadRequest) - return nil, err + return nil, false } - - copts.setHeader(w.Header()) - - return copts, nil + return copts, true } func headerContainsTokenIgnoreCase(h http.Header, key, token string) bool { diff --git a/accept_test.go b/accept_test.go index ae17c0b4..513313ec 100644 --- a/accept_test.go +++ b/accept_test.go @@ -62,20 +62,50 @@ func TestAccept(t *testing.T) { t.Run("badCompression", func(t *testing.T) { t.Parallel() - w := mockHijacker{ - ResponseWriter: httptest.NewRecorder(), + newRequest := func(extensions string) *http.Request { + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Extensions", extensions) + return r + } + errHijack := errors.New("hijack error") + newResponseWriter := func() http.ResponseWriter { + return mockHijacker{ + ResponseWriter: httptest.NewRecorder(), + hijack: func() (net.Conn, *bufio.ReadWriter, error) { + return nil, nil, errHijack + }, + } } - r := httptest.NewRequest("GET", "/", nil) - r.Header.Set("Connection", "Upgrade") - r.Header.Set("Upgrade", "websocket") - r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") - r.Header.Set("Sec-WebSocket-Extensions", "permessage-deflate; harharhar") - _, err := Accept(w, r, &AcceptOptions{ - CompressionMode: CompressionContextTakeover, + t.Run("withoutFallback", func(t *testing.T) { + t.Parallel() + + w := newResponseWriter() + r := newRequest("permessage-deflate; harharhar") + _, err := Accept(w, r, &AcceptOptions{ + CompressionMode: CompressionNoContextTakeover, + }) + assert.ErrorIs(t, errHijack, err) + assert.Equal(t, "extension header", w.Header().Get("Sec-WebSocket-Extensions"), "") + }) + t.Run("withFallback", func(t *testing.T) { + t.Parallel() + + w := newResponseWriter() + r := newRequest("permessage-deflate; harharhar, permessage-deflate") + _, err := Accept(w, r, &AcceptOptions{ + CompressionMode: CompressionNoContextTakeover, + }) + assert.ErrorIs(t, errHijack, err) + assert.Equal(t, "extension header", + w.Header().Get("Sec-WebSocket-Extensions"), + CompressionNoContextTakeover.opts().String(), + ) }) - assert.Contains(t, err, `unsupported permessage-deflate parameter`) }) t.Run("requireHttpHijacker", func(t *testing.T) { @@ -344,42 +374,53 @@ func Test_authenticateOrigin(t *testing.T) { } } -func Test_acceptCompression(t *testing.T) { +func Test_selectDeflate(t *testing.T) { t.Parallel() testCases := []struct { - name string - mode CompressionMode - reqSecWebSocketExtensions string - respSecWebSocketExtensions string - expCopts *compressionOptions - error bool + name string + mode CompressionMode + header string + expCopts *compressionOptions + expOK bool }{ { name: "disabled", mode: CompressionDisabled, expCopts: nil, + expOK: false, }, { name: "noClientSupport", mode: CompressionNoContextTakeover, expCopts: nil, + expOK: false, }, { - name: "permessage-deflate", - mode: CompressionNoContextTakeover, - reqSecWebSocketExtensions: "permessage-deflate; client_max_window_bits", - respSecWebSocketExtensions: "permessage-deflate; client_no_context_takeover; server_no_context_takeover", + name: "permessage-deflate", + mode: CompressionNoContextTakeover, + header: "permessage-deflate; client_max_window_bits", expCopts: &compressionOptions{ clientNoContextTakeover: true, serverNoContextTakeover: true, }, + expOK: true, + }, + { + name: "permessage-deflate/unknown-parameter", + mode: CompressionNoContextTakeover, + header: "permessage-deflate; meow", + expOK: false, }, { - name: "permessage-deflate/error", - mode: CompressionNoContextTakeover, - reqSecWebSocketExtensions: "permessage-deflate; meow", - error: true, + name: "permessage-deflate/unknown-parameter", + mode: CompressionNoContextTakeover, + header: "permessage-deflate; meow, permessage-deflate; client_max_window_bits", + expCopts: &compressionOptions{ + clientNoContextTakeover: true, + serverNoContextTakeover: true, + }, + expOK: true, }, // { // name: "x-webkit-deflate-frame", @@ -404,19 +445,11 @@ func Test_acceptCompression(t *testing.T) { t.Run(tc.name, func(t *testing.T) { t.Parallel() - r := httptest.NewRequest(http.MethodGet, "/", nil) - r.Header.Set("Sec-WebSocket-Extensions", tc.reqSecWebSocketExtensions) - - w := httptest.NewRecorder() - copts, err := acceptCompression(r, w, tc.mode) - if tc.error { - assert.Error(t, err) - return - } - - assert.Success(t, err) + h := http.Header{} + h.Set("Sec-WebSocket-Extensions", tc.header) + copts, ok := selectDeflate(websocketExtensions(h), tc.mode) + assert.Equal(t, "selected options", tc.expOK, ok) assert.Equal(t, "compression options", tc.expCopts, copts) - assert.Equal(t, "Sec-WebSocket-Extensions", tc.respSecWebSocketExtensions, w.Header().Get("Sec-WebSocket-Extensions")) }) } } diff --git a/compress.go b/compress.go index 61e6e268..ee21e1d1 100644 --- a/compress.go +++ b/compress.go @@ -6,7 +6,6 @@ package websocket import ( "compress/flate" "io" - "net/http" "sync" ) @@ -65,7 +64,7 @@ type compressionOptions struct { serverNoContextTakeover bool } -func (copts *compressionOptions) setHeader(h http.Header) { +func (copts *compressionOptions) String() string { s := "permessage-deflate" if copts.clientNoContextTakeover { s += "; client_no_context_takeover" @@ -73,7 +72,7 @@ func (copts *compressionOptions) setHeader(h http.Header) { if copts.serverNoContextTakeover { s += "; server_no_context_takeover" } - h.Set("Sec-WebSocket-Extensions", s) + return s } // These bytes are required to get flate.Reader to return. diff --git a/dial.go b/dial.go index 9acca133..e72432e7 100644 --- a/dial.go +++ b/dial.go @@ -185,7 +185,7 @@ func handshakeRequest(ctx context.Context, urls string, opts *DialOptions, copts req.Header.Set("Sec-WebSocket-Protocol", strings.Join(opts.Subprotocols, ",")) } if copts != nil { - copts.setHeader(req.Header) + req.Header.Set("Sec-WebSocket-Extensions", copts.String()) } resp, err := opts.HTTPClient.Do(req) diff --git a/internal/test/assert/assert.go b/internal/test/assert/assert.go index 64c938c5..1b90cc9f 100644 --- a/internal/test/assert/assert.go +++ b/internal/test/assert/assert.go @@ -1,6 +1,7 @@ package assert import ( + "errors" "fmt" "reflect" "strings" @@ -43,3 +44,12 @@ func Contains(t testing.TB, v interface{}, sub string) { t.Fatalf("expected %q to contain %q", s, sub) } } + +// ErrorIs asserts errors.Is(got, exp) +func ErrorIs(t testing.TB, exp, got error) { + t.Helper() + + if !errors.Is(got, exp) { + t.Fatalf("expected %v but got %v", exp, got) + } +} From d6b342b14042413308040566fcfd0d3f3ea85d10 Mon Sep 17 00:00:00 2001 From: Andy Bursavich Date: Tue, 8 Sep 2020 19:30:22 -0700 Subject: [PATCH 066/152] Remove x-webkit-deflate-frame dead code --- accept_test.go | 16 ---------------- compress.go | 6 +----- ws_js.go | 7 +------ 3 files changed, 2 insertions(+), 27 deletions(-) diff --git a/accept_test.go b/accept_test.go index 513313ec..c554bdaf 100644 --- a/accept_test.go +++ b/accept_test.go @@ -422,22 +422,6 @@ func Test_selectDeflate(t *testing.T) { }, expOK: true, }, - // { - // name: "x-webkit-deflate-frame", - // mode: CompressionNoContextTakeover, - // reqSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", - // respSecWebSocketExtensions: "x-webkit-deflate-frame; no_context_takeover", - // expCopts: &compressionOptions{ - // clientNoContextTakeover: true, - // serverNoContextTakeover: true, - // }, - // }, - // { - // name: "x-webkit-deflate/error", - // mode: CompressionNoContextTakeover, - // reqSecWebSocketExtensions: "x-webkit-deflate-frame; max_window_bits", - // error: true, - // }, } for _, tc := range testCases { diff --git a/compress.go b/compress.go index ee21e1d1..81de751b 100644 --- a/compress.go +++ b/compress.go @@ -12,11 +12,7 @@ import ( // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 // -// A compatibility layer is implemented for the older deflate-frame extension used -// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 -// It will work the same in every way except that we cannot signal to the peer we -// want to use no context takeover on our side, we can only signal that they should. -// But it is currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 +// Works in all browsers except Safari which does not implement the deflate extension. type CompressionMode int const ( diff --git a/ws_js.go b/ws_js.go index 9f0e19e9..e60601e3 100644 --- a/ws_js.go +++ b/ws_js.go @@ -485,12 +485,7 @@ func CloseStatus(err error) StatusCode { // CompressionMode represents the modes available to the deflate extension. // See https://tools.ietf.org/html/rfc7692 -// -// A compatibility layer is implemented for the older deflate-frame extension used -// by safari. See https://tools.ietf.org/html/draft-tyoshino-hybi-websocket-perframe-deflate-06 -// It will work the same in every way except that we cannot signal to the peer we -// want to use no context takeover on our side, we can only signal that they should. -// It is however currently disabled due to Safari bugs. See https://github.com/nhooyr/websocket/issues/218 +// Works in all browsers except Safari which does not implement the deflate extension. type CompressionMode int const ( From a975390c8cd69948d2e3e8f0665aaf131400f550 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 12:53:12 -0700 Subject: [PATCH 067/152] internal/*/go.mod: Use go 1.19 too --- internal/examples/go.mod | 2 +- internal/thirdparty/go.mod | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/internal/examples/go.mod b/internal/examples/go.mod index ef4c5f67..b5cdcc1d 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket/examples -go 1.22 +go 1.19 replace nhooyr.io/websocket => ../.. diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index b0a979f5..e8c3e2c0 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -1,6 +1,6 @@ module nhooyr.io/websocket/internal/thirdparty -go 1.22 +go 1.19 replace nhooyr.io/websocket => ../.. From 1dbc1412d602060f6362a66fdc181da79b8136a4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 18:17:02 -0700 Subject: [PATCH 068/152] write: Zero alloc writes with Writer Closes #354 --- .gitignore | 1 - ci/bench.sh | 4 ++-- conn.go | 9 ++++---- write.go | 62 +++++++++++++++++++++-------------------------------- 4 files changed, 31 insertions(+), 45 deletions(-) delete mode 100644 .gitignore diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 6961e5c8..00000000 --- a/.gitignore +++ /dev/null @@ -1 +0,0 @@ -websocket.test diff --git a/ci/bench.sh b/ci/bench.sh index 8f99278d..a553b93a 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -2,8 +2,8 @@ set -eu cd -- "$(dirname "$0")/.." -go test --run=^$ --bench=. "$@" ./... +go test --run=^$ --bench=. --benchmem --memprofile ci/out/prof.mem --cpuprofile ci/out/prof.cpu -o ci/out/websocket.test "$@" . ( cd ./internal/thirdparty - go test --run=^$ --bench=. "$@" ./... + go test --run=^$ --bench=. --benchmem --memprofile ../../ci/out/prof-thirdparty.mem --cpuprofile ../../ci/out/prof-thirdparty.cpu -o ../../ci/out/thirdparty.test "$@" . ) diff --git a/conn.go b/conn.go index 81a57c7f..78eaad82 100644 --- a/conn.go +++ b/conn.go @@ -63,7 +63,7 @@ type Conn struct { readCloseFrameErr error // Write state. - msgWriterState *msgWriterState + msgWriter *msgWriter writeFrameMu *mu writeBuf []byte writeHeaderBuf [8]byte @@ -113,14 +113,14 @@ func newConn(cfg connConfig) *Conn { c.msgReader = newMsgReader(c) - c.msgWriterState = newMsgWriterState(c) + c.msgWriter = newMsgWriter(c) if c.client { c.writeBuf = extractBufioWriterBuf(c.bw, c.rwc) } if c.flate() && c.flateThreshold == 0 { c.flateThreshold = 128 - if !c.msgWriterState.flateContextTakeover() { + if !c.msgWriter.flateContextTakeover() { c.flateThreshold = 512 } } @@ -157,8 +157,7 @@ func (c *Conn) close(err error) { c.rwc.Close() go func() { - c.msgWriterState.close() - + c.msgWriter.close() c.msgReader.close() }() } diff --git a/write.go b/write.go index 500609dd..20a71d3e 100644 --- a/write.go +++ b/write.go @@ -49,30 +49,11 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { } type msgWriter struct { - mw *msgWriterState - closed bool -} - -func (mw *msgWriter) Write(p []byte) (int, error) { - if mw.closed { - return 0, errors.New("cannot use closed writer") - } - return mw.mw.Write(p) -} - -func (mw *msgWriter) Close() error { - if mw.closed { - return errors.New("cannot use closed writer") - } - mw.closed = true - return mw.mw.Close() -} - -type msgWriterState struct { c *Conn mu *mu writeMu *mu + closed bool ctx context.Context opcode opcode @@ -82,8 +63,8 @@ type msgWriterState struct { flateWriter *flate.Writer } -func newMsgWriterState(c *Conn) *msgWriterState { - mw := &msgWriterState{ +func newMsgWriter(c *Conn) *msgWriter { + mw := &msgWriter{ c: c, mu: newMu(c), writeMu: newMu(c), @@ -91,7 +72,7 @@ func newMsgWriterState(c *Conn) *msgWriterState { return mw } -func (mw *msgWriterState) ensureFlate() { +func (mw *msgWriter) ensureFlate() { if mw.trimWriter == nil { mw.trimWriter = &trimLastFourBytesWriter{ w: util.WriterFunc(mw.write), @@ -104,7 +85,7 @@ func (mw *msgWriterState) ensureFlate() { mw.flate = true } -func (mw *msgWriterState) flateContextTakeover() bool { +func (mw *msgWriter) flateContextTakeover() bool { if mw.c.client { return !mw.c.copts.clientNoContextTakeover } @@ -112,14 +93,11 @@ func (mw *msgWriterState) flateContextTakeover() bool { } func (c *Conn) writer(ctx context.Context, typ MessageType) (io.WriteCloser, error) { - err := c.msgWriterState.reset(ctx, typ) + err := c.msgWriter.reset(ctx, typ) if err != nil { return nil, err } - return &msgWriter{ - mw: c.msgWriterState, - closed: false, - }, nil + return c.msgWriter, nil } func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error) { @@ -129,8 +107,8 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error } if !c.flate() { - defer c.msgWriterState.mu.unlock() - return c.writeFrame(ctx, true, false, c.msgWriterState.opcode, p) + defer c.msgWriter.mu.unlock() + return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) } n, err := mw.Write(p) @@ -142,7 +120,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error return n, err } -func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { +func (mw *msgWriter) reset(ctx context.Context, typ MessageType) error { err := mw.mu.lock(ctx) if err != nil { return err @@ -151,13 +129,14 @@ func (mw *msgWriterState) reset(ctx context.Context, typ MessageType) error { mw.ctx = ctx mw.opcode = opcode(typ) mw.flate = false + mw.closed = false mw.trimWriter.reset() return nil } -func (mw *msgWriterState) putFlateWriter() { +func (mw *msgWriter) putFlateWriter() { if mw.flateWriter != nil { putFlateWriter(mw.flateWriter) mw.flateWriter = nil @@ -165,7 +144,11 @@ func (mw *msgWriterState) putFlateWriter() { } // Write writes the given bytes to the WebSocket connection. -func (mw *msgWriterState) Write(p []byte) (_ int, err error) { +func (mw *msgWriter) Write(p []byte) (_ int, err error) { + if mw.closed { + return 0, errors.New("cannot use closed writer") + } + err = mw.writeMu.lock(mw.ctx) if err != nil { return 0, fmt.Errorf("failed to write: %w", err) @@ -194,7 +177,7 @@ func (mw *msgWriterState) Write(p []byte) (_ int, err error) { return mw.write(p) } -func (mw *msgWriterState) write(p []byte) (int, error) { +func (mw *msgWriter) write(p []byte) (int, error) { n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { return n, fmt.Errorf("failed to write data frame: %w", err) @@ -204,9 +187,14 @@ func (mw *msgWriterState) write(p []byte) (int, error) { } // Close flushes the frame to the connection. -func (mw *msgWriterState) Close() (err error) { +func (mw *msgWriter) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") + if mw.closed { + return errors.New("writer already closed") + } + mw.closed = true + err = mw.writeMu.lock(mw.ctx) if err != nil { return err @@ -232,7 +220,7 @@ func (mw *msgWriterState) Close() (err error) { return nil } -func (mw *msgWriterState) close() { +func (mw *msgWriter) close() { if mw.c.client { mw.c.writeFrameMu.forceLock() putBufioWriter(mw.c.bw) From a94999fb3a308b562b13c85f4d458564adea9147 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 13 Oct 2023 18:28:04 -0700 Subject: [PATCH 069/152] close: Implement CloseNow Closes #384 --- close.go | 15 ++++++++++++++- conn.go | 3 +++ conn_test.go | 13 +++++++++++++ export_test.go | 2 ++ 4 files changed, 32 insertions(+), 1 deletion(-) diff --git a/close.go b/close.go index 1e13ca73..25160ee1 100644 --- a/close.go +++ b/close.go @@ -102,6 +102,19 @@ func (c *Conn) Close(code StatusCode, reason string) error { return c.closeHandshake(code, reason) } +// CloseNow closes the WebSocket connection without attempting a close handshake. +// Use When you do not want the overhead of the close handshake. +func (c *Conn) CloseNow() (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + if c.isClosed() { + return errClosed + } + + c.close(nil) + return c.closeErr +} + func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { defer errd.Wrap(&err, "failed to close WebSocket") @@ -265,7 +278,7 @@ func (c *Conn) setCloseErr(err error) { } func (c *Conn) setCloseErrLocked(err error) { - if c.closeErr == nil { + if c.closeErr == nil && err != nil { c.closeErr = fmt.Errorf("WebSocket closed: %w", err) } } diff --git a/conn.go b/conn.go index 78eaad82..3713b1f8 100644 --- a/conn.go +++ b/conn.go @@ -147,6 +147,9 @@ func (c *Conn) close(err error) { if c.isClosed() { return } + if err == nil { + err = c.rwc.Close() + } c.setCloseErrLocked(err) close(c.closed) runtime.SetFinalizer(c, nil) diff --git a/conn_test.go b/conn_test.go index 7a6a0c39..50b844b9 100644 --- a/conn_test.go +++ b/conn_test.go @@ -295,6 +295,19 @@ func TestConn(t *testing.T) { err = c1.Close(websocket.StatusNormalClosure, "") assert.Success(t, err) }) + + t.Run("CloseNow", func(t *testing.T) { + _, c1, c2 := newConnTest(t, nil, nil) + + err1 := c1.CloseNow() + err2 := c2.CloseNow() + assert.Success(t, err1) + assert.Success(t, err2) + err1 = c1.CloseNow() + err2 = c2.CloseNow() + assert.ErrorIs(t, websocket.ErrClosed, err1) + assert.ErrorIs(t, websocket.ErrClosed, err2) + }) } func TestWasm(t *testing.T) { diff --git a/export_test.go b/export_test.go index 8731b6d8..114796d0 100644 --- a/export_test.go +++ b/export_test.go @@ -23,3 +23,5 @@ func (c *Conn) RecordBytesRead() *int { })) return &bytesRead } + +var ErrClosed = errClosed From e314da6c5e9edeaa1457dd9d869ff080b07c54f5 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 14 Oct 2023 06:13:10 -0700 Subject: [PATCH 070/152] dial: Redirect wss/ws correctly by modifying the http client Closes #333 --- dial.go | 15 +++++++++++++++ dial_test.go | 25 +++++++++++++++++++++++++ 2 files changed, 40 insertions(+) diff --git a/dial.go b/dial.go index e72432e7..e4c4daa1 100644 --- a/dial.go +++ b/dial.go @@ -70,6 +70,21 @@ func (opts *DialOptions) cloneWithDefaults(ctx context.Context) (context.Context if o.HTTPHeader == nil { o.HTTPHeader = http.Header{} } + newClient := *o.HTTPClient + oldCheckRedirect := o.HTTPClient.CheckRedirect + newClient.CheckRedirect = func(req *http.Request, via []*http.Request) error { + switch req.URL.Scheme { + case "ws": + req.URL.Scheme = "http" + case "wss": + req.URL.Scheme = "https" + } + if oldCheckRedirect != nil { + return oldCheckRedirect(req, via) + } + return nil + } + o.HTTPClient = &newClient return ctx, cancel, &o } diff --git a/dial_test.go b/dial_test.go index e072db2d..3652f8d4 100644 --- a/dial_test.go +++ b/dial_test.go @@ -304,3 +304,28 @@ type roundTripperFunc func(*http.Request) (*http.Response, error) func (f roundTripperFunc) RoundTrip(r *http.Request) (*http.Response, error) { return f(r) } + +func TestDialRedirect(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() + + _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + HTTPClient: mockHTTPClient(func(r *http.Request) (*http.Response, error) { + resp := &http.Response{ + Header: http.Header{}, + } + if r.URL.Scheme != "https" { + resp.Header.Set("Location", "wss://example.com") + resp.StatusCode = http.StatusFound + return resp, nil + } + resp.Header.Set("Connection", "Upgrade") + resp.Header.Set("Upgrade", "meow") + resp.StatusCode = http.StatusSwitchingProtocols + return resp, nil + }), + }) + assert.Contains(t, err, "failed to WebSocket dial: WebSocket protocol violation: Upgrade header \"meow\" does not contain websocket") +} From 249edb209389a1b6fd3b1f79de78417982077284 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 20:44:27 -0700 Subject: [PATCH 071/152] dial_test: Add TestDialViaProxy For #395 Somehow currently reproduces #391... Debugging still. --- conn_test.go | 26 ++++++++++++ dial_test.go | 110 ++++++++++++++++++++++++++++++++++++++++++------- export_test.go | 7 ++++ 3 files changed, 128 insertions(+), 15 deletions(-) diff --git a/conn_test.go b/conn_test.go index 50b844b9..5f78cad5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -526,3 +526,29 @@ func echoServer(w http.ResponseWriter, r *http.Request, opts *websocket.AcceptOp err = wstest.EchoLoop(r.Context(), c) return assertCloseStatus(websocket.StatusNormalClosure, err) } + +func assertEcho(tb testing.TB, ctx context.Context, c *websocket.Conn) { + exp := xrand.String(xrand.Int(131072)) + + werr := xsync.Go(func() error { + return wsjson.Write(ctx, c, exp) + }) + + var act interface{} + err := wsjson.Read(ctx, c, &act) + assert.Success(tb, err) + assert.Equal(tb, "read msg", exp, act) + + select { + case err := <-werr: + assert.Success(tb, err) + case <-ctx.Done(): + tb.Fatal(ctx.Err()) + } +} + +func assertClose(tb testing.TB, c *websocket.Conn) { + tb.Helper() + err := c.Close(websocket.StatusNormalClosure, "") + assert.Success(tb, err) +} diff --git a/dial_test.go b/dial_test.go index 3652f8d4..7a84436d 100644 --- a/dial_test.go +++ b/dial_test.go @@ -1,7 +1,7 @@ //go:build !js // +build !js -package websocket +package websocket_test import ( "bytes" @@ -10,12 +10,15 @@ import ( "io" "net/http" "net/http/httptest" + "net/url" "strings" "testing" "time" + "nhooyr.io/websocket" "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/util" + "nhooyr.io/websocket/internal/xsync" ) func TestBadDials(t *testing.T) { @@ -27,7 +30,7 @@ func TestBadDials(t *testing.T) { testCases := []struct { name string url string - opts *DialOptions + opts *websocket.DialOptions rand util.ReaderFunc nilCtx bool }{ @@ -72,7 +75,7 @@ func TestBadDials(t *testing.T) { tc.rand = rand.Reader.Read } - _, _, err := dial(ctx, tc.url, tc.opts, tc.rand) + _, _, err := websocket.ExportedDial(ctx, tc.url, tc.opts, tc.rand) assert.Error(t, err) }) } @@ -84,7 +87,7 @@ func TestBadDials(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(func(*http.Request) (*http.Response, error) { return &http.Response{ Body: io.NopCloser(strings.NewReader("hi")), @@ -104,7 +107,7 @@ func TestBadDials(t *testing.T) { h := http.Header{} h.Set("Connection", "Upgrade") h.Set("Upgrade", "websocket") - h.Set("Sec-WebSocket-Accept", secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) + h.Set("Sec-WebSocket-Accept", websocket.SecWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) return &http.Response{ StatusCode: http.StatusSwitchingProtocols, @@ -113,7 +116,7 @@ func TestBadDials(t *testing.T) { }, nil } - _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(rt), }) assert.Contains(t, err, "response body is not a io.ReadWriteCloser") @@ -152,7 +155,7 @@ func Test_verifyHostOverride(t *testing.T) { h := http.Header{} h.Set("Connection", "Upgrade") h.Set("Upgrade", "websocket") - h.Set("Sec-WebSocket-Accept", secWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) + h.Set("Sec-WebSocket-Accept", websocket.SecWebSocketAccept(r.Header.Get("Sec-WebSocket-Key"))) return &http.Response{ StatusCode: http.StatusSwitchingProtocols, @@ -161,7 +164,7 @@ func Test_verifyHostOverride(t *testing.T) { }, nil } - _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(rt), Host: tc.host, }) @@ -272,18 +275,18 @@ func Test_verifyServerHandshake(t *testing.T) { resp := w.Result() r := httptest.NewRequest("GET", "/", nil) - key, err := secWebSocketKey(rand.Reader) + key, err := websocket.SecWebSocketKey(rand.Reader) assert.Success(t, err) r.Header.Set("Sec-WebSocket-Key", key) if resp.Header.Get("Sec-WebSocket-Accept") == "" { - resp.Header.Set("Sec-WebSocket-Accept", secWebSocketAccept(key)) + resp.Header.Set("Sec-WebSocket-Accept", websocket.SecWebSocketAccept(key)) } - opts := &DialOptions{ + opts := &websocket.DialOptions{ Subprotocols: strings.Split(r.Header.Get("Sec-WebSocket-Protocol"), ","), } - _, err = verifyServerResponse(opts, opts.CompressionMode.opts(), key, resp) + _, err = websocket.VerifyServerResponse(opts, websocket.CompressionModeOpts(opts.CompressionMode), key, resp) if tc.success { assert.Success(t, err) } else { @@ -311,7 +314,7 @@ func TestDialRedirect(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() - _, _, err := Dial(ctx, "ws://example.com", &DialOptions{ + _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(func(r *http.Request) (*http.Response, error) { resp := &http.Response{ Header: http.Header{}, @@ -321,11 +324,88 @@ func TestDialRedirect(t *testing.T) { resp.StatusCode = http.StatusFound return resp, nil } - resp.Header.Set("Connection", "Upgrade") - resp.Header.Set("Upgrade", "meow") + resp.Header.Set("Connection", "Upgrade") + resp.Header.Set("Upgrade", "meow") resp.StatusCode = http.StatusSwitchingProtocols return resp, nil }), }) assert.Contains(t, err, "failed to WebSocket dial: WebSocket protocol violation: Upgrade header \"meow\" does not contain websocket") } + +type forwardProxy struct { + hc *http.Client +} + +func newForwardProxy() *forwardProxy { + return &forwardProxy{ + hc: &http.Client{}, + } +} + +func (fc *forwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { + ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) + defer cancel() + + r = r.WithContext(ctx) + r.RequestURI = "" + resp, err := fc.hc.Do(r) + if err != nil { + http.Error(w, err.Error(), http.StatusBadRequest) + return + } + defer resp.Body.Close() + + for k, v := range resp.Header { + w.Header()[k] = v + } + w.Header().Set("PROXIED", "true") + w.WriteHeader(resp.StatusCode) + errc1 := xsync.Go(func() error { + _, err := io.Copy(w, resp.Body) + return err + }) + var errc2 <-chan error + if bodyw, ok := resp.Body.(io.Writer); ok { + errc2 = xsync.Go(func() error { + _, err := io.Copy(bodyw, r.Body) + return err + }) + } + select { + case <-errc1: + case <-errc2: + case <-r.Context().Done(): + } +} + +func TestDialViaProxy(t *testing.T) { + t.Parallel() + + ps := httptest.NewServer(newForwardProxy()) + defer ps.Close() + + s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + err := echoServer(w, r, nil) + assert.Success(t, err) + })) + defer s.Close() + + psu, err := url.Parse(ps.URL) + assert.Success(t, err) + proxyTransport := http.DefaultTransport.(*http.Transport).Clone() + proxyTransport.Proxy = http.ProxyURL(psu) + + ctx, cancel := context.WithTimeout(context.Background(), time.Second*10) + defer cancel() + c, resp, err := websocket.Dial(ctx, s.URL, &websocket.DialOptions{ + HTTPClient: &http.Client{ + Transport: proxyTransport, + }, + }) + assert.Success(t, err) + assert.Equal(t, "", "true", resp.Header.Get("PROXIED")) + + assertEcho(t, ctx, c) + assertClose(t, c) +} diff --git a/export_test.go b/export_test.go index 114796d0..e322c36f 100644 --- a/export_test.go +++ b/export_test.go @@ -25,3 +25,10 @@ func (c *Conn) RecordBytesRead() *int { } var ErrClosed = errClosed + +var ExportedDial = dial +var SecWebSocketAccept = secWebSocketAccept +var SecWebSocketKey = secWebSocketKey +var VerifyServerResponse = verifyServerResponse + +var CompressionModeOpts = CompressionMode.opts From 818579b7f9eb42c34dceb41d2b113d382cede0df Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:05:02 -0700 Subject: [PATCH 072/152] TestDialViaProxy: Fix bug in forward proxy Closes #395 Confirmed library works correctly with a working forward proxy. --- conn_test.go | 1 + dial_test.go | 34 +++++++++++++++++++++------------- 2 files changed, 22 insertions(+), 13 deletions(-) diff --git a/conn_test.go b/conn_test.go index 5f78cad5..c814ca28 100644 --- a/conn_test.go +++ b/conn_test.go @@ -535,6 +535,7 @@ func assertEcho(tb testing.TB, ctx context.Context, c *websocket.Conn) { }) var act interface{} + c.SetReadLimit(1 << 30) err := wsjson.Read(ctx, c, &act) assert.Success(tb, err) assert.Equal(tb, "read msg", exp, act) diff --git a/dial_test.go b/dial_test.go index 7a84436d..63cb4be6 100644 --- a/dial_test.go +++ b/dial_test.go @@ -361,21 +361,29 @@ func (fc *forwardProxy) ServeHTTP(w http.ResponseWriter, r *http.Request) { } w.Header().Set("PROXIED", "true") w.WriteHeader(resp.StatusCode) - errc1 := xsync.Go(func() error { - _, err := io.Copy(w, resp.Body) - return err - }) - var errc2 <-chan error - if bodyw, ok := resp.Body.(io.Writer); ok { - errc2 = xsync.Go(func() error { - _, err := io.Copy(bodyw, r.Body) + if resprw, ok := resp.Body.(io.ReadWriter); ok { + c, brw, err := w.(http.Hijacker).Hijack() + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + brw.Flush() + + errc1 := xsync.Go(func() error { + _, err := io.Copy(c, resprw) return err }) - } - select { - case <-errc1: - case <-errc2: - case <-r.Context().Done(): + errc2 := xsync.Go(func() error { + _, err := io.Copy(resprw, c) + return err + }) + select { + case <-errc1: + case <-errc2: + case <-r.Context().Done(): + } + } else { + io.Copy(w, resp.Body) } } From 20b883815e581f10639cf96d6132dcdac92b596a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:11:33 -0700 Subject: [PATCH 073/152] ci: Add dev to daily --- .github/workflows/daily.yml | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index d10c142f..b625fd68 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -23,7 +23,31 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh + - run: AUTOBAHN=1 ./ci/test.sh -bench=. + - uses: actions/upload-artifact@v3 + with: + name: coverage.html + path: ./ci/out/coverage.html + bench-dev: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: dev + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: AUTOBAHN=1 ./ci/bench.sh + test-dev: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + ref: dev + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: AUTOBAHN=1 ./ci/test.sh -bench=. - uses: actions/upload-artifact@v3 with: name: coverage.html From 591ff8e56211cab65a30d6bd5efa0902719a92ea Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:12:41 -0700 Subject: [PATCH 074/152] accept.go: Comment typo --- accept.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/accept.go b/accept.go index 19e388ec..b90e15eb 100644 --- a/accept.go +++ b/accept.go @@ -244,7 +244,7 @@ func selectDeflate(extensions []websocketExtension, mode CompressionMode) (*comp } for _, ext := range extensions { switch ext.name { - // We used to implement x-webkit-deflate-fram too but Safari has bugs. + // We used to implement x-webkit-deflate-frame too for Safari but Safari has bugs... // See https://github.com/nhooyr/websocket/issues/218 case "permessage-deflate": copts, ok := acceptDeflate(ext, mode) From 64ce00991a066009cdeb34971f3c21ebb3e2766f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:17:07 -0700 Subject: [PATCH 075/152] conn: Return net.ErrClosed whenever appropriate Updates e9d08816010996a14241f008ac097c5621bd1f30 --- close.go | 2 +- conn.go | 6 +++--- make.sh | 2 +- read.go | 12 ++++++------ write.go | 8 ++++---- 5 files changed, 15 insertions(+), 15 deletions(-) diff --git a/close.go b/close.go index 25160ee1..24907c64 100644 --- a/close.go +++ b/close.go @@ -125,7 +125,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return writeErr } - if CloseStatus(closeHandshakeErr) == -1 { + if CloseStatus(closeHandshakeErr) == -1 && !errors.Is(errClosed, closeHandshakeErr) { return closeHandshakeErr } diff --git a/conn.go b/conn.go index 3713b1f8..36662a93 100644 --- a/conn.go +++ b/conn.go @@ -228,7 +228,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { select { case <-c.closed: - return c.closeErr + return errClosed case <-ctx.Done(): err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) c.close(err) @@ -266,7 +266,7 @@ func (m *mu) tryLock() bool { func (m *mu) lock(ctx context.Context) error { select { case <-m.c.closed: - return m.c.closeErr + return errClosed case <-ctx.Done(): err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) m.c.close(err) @@ -279,7 +279,7 @@ func (m *mu) lock(ctx context.Context) error { case <-m.c.closed: // Make sure to release. m.unlock() - return m.c.closeErr + return errClosed default: } return nil diff --git a/make.sh b/make.sh index 68a98ac1..81909d72 100755 --- a/make.sh +++ b/make.sh @@ -4,5 +4,5 @@ cd -- "$(dirname "$0")" ./ci/fmt.sh ./ci/lint.sh -./ci/test.sh +./ci/test.sh "$@" ./ci/bench.sh diff --git a/read.go b/read.go index d3217861..bf4362df 100644 --- a/read.go +++ b/read.go @@ -203,7 +203,7 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { case <-c.closed: - return header{}, c.closeErr + return header{}, errClosed case c.readTimeout <- ctx: } @@ -211,7 +211,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { if err != nil { select { case <-c.closed: - return header{}, c.closeErr + return header{}, errClosed case <-ctx.Done(): return header{}, ctx.Err() default: @@ -222,7 +222,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { case <-c.closed: - return header{}, c.closeErr + return header{}, errClosed case c.readTimeout <- context.Background(): } @@ -232,7 +232,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { case <-c.closed: - return 0, c.closeErr + return 0, errClosed case c.readTimeout <- ctx: } @@ -240,7 +240,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { if err != nil { select { case <-c.closed: - return n, c.closeErr + return n, errClosed case <-ctx.Done(): return n, ctx.Err() default: @@ -252,7 +252,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { case <-c.closed: - return n, c.closeErr + return n, errClosed case c.readTimeout <- context.Background(): } diff --git a/write.go b/write.go index 20a71d3e..b7cf6600 100644 --- a/write.go +++ b/write.go @@ -262,14 +262,14 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco case <-ctx.Done(): return 0, ctx.Err() case <-c.closed: - return 0, c.closeErr + return 0, errClosed } } defer c.writeFrameMu.unlock() select { case <-c.closed: - return 0, c.closeErr + return 0, errClosed case c.writeTimeout <- ctx: } @@ -277,7 +277,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if err != nil { select { case <-c.closed: - err = c.closeErr + err = errClosed case <-ctx.Done(): err = ctx.Err() } @@ -323,7 +323,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco select { case <-c.closed: - return n, c.closeErr + return n, errClosed case c.writeTimeout <- context.Background(): } From 1a344a4c1349d0947dcb6346ea2d35523625a265 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 20 Dec 2022 14:16:01 -0600 Subject: [PATCH 076/152] Reject invalid "Sec-WebSocket-Key" headers from clients Client "Sec-WebSocket-Key" should be a valid 16 byte base64 encoded nonce. If the header is not valid, the server should reject the client. --- accept.go | 7 ++++++- accept_test.go | 33 ++++++++++++++++++++++++++------- internal/test/xrand/xrand.go | 5 +++++ 3 files changed, 37 insertions(+), 8 deletions(-) diff --git a/accept.go b/accept.go index b90e15eb..2f4fb2eb 100644 --- a/accept.go +++ b/accept.go @@ -185,10 +185,15 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) } - if r.Header.Get("Sec-WebSocket-Key") == "" { + websocketSecKey := r.Header.Get("Sec-WebSocket-Key") + if websocketSecKey == "" { return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } + if v, err := base64.StdEncoding.DecodeString(websocketSecKey); err != nil || len(v) != 16 { + return http.StatusBadRequest, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Key %q, must be a 16 byte base64 encoded string", websocketSecKey) + } + return 0, nil } diff --git a/accept_test.go b/accept_test.go index c554bdaf..d0cc4878 100644 --- a/accept_test.go +++ b/accept_test.go @@ -9,6 +9,7 @@ import ( "net" "net/http" "net/http/httptest" + "nhooyr.io/websocket/internal/test/xrand" "strings" "testing" @@ -36,7 +37,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) r.Header.Set("Origin", "harhar.com") _, err := Accept(w, r, nil) @@ -52,7 +53,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) r.Header.Set("Origin", "https://harhar.com") _, err := Accept(w, r, nil) @@ -116,7 +117,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) _, err := Accept(w, r, nil) assert.Contains(t, err, `http.ResponseWriter does not implement http.Hijacker`) @@ -136,7 +137,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) _, err := Accept(w, r, nil) assert.Contains(t, err, `failed to hijack connection`) @@ -183,7 +184,7 @@ func Test_verifyClientHandshake(t *testing.T) { }, }, { - name: "badWebSocketKey", + name: "missingWebSocketKey", h: map[string]string{ "Connection": "Upgrade", "Upgrade": "websocket", @@ -191,13 +192,31 @@ func Test_verifyClientHandshake(t *testing.T) { "Sec-WebSocket-Key": "", }, }, + { + name: "shortWebSocketKey", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + "Sec-WebSocket-Key": xrand.Base64(15), + }, + }, + { + name: "invalidWebSocketKey", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + "Sec-WebSocket-Key": "notbase64", + }, + }, { name: "badHTTPVersion", h: map[string]string{ "Connection": "Upgrade", "Upgrade": "websocket", "Sec-WebSocket-Version": "13", - "Sec-WebSocket-Key": "meow123", + "Sec-WebSocket-Key": xrand.Base64(16), }, http1: true, }, @@ -207,7 +226,7 @@ func Test_verifyClientHandshake(t *testing.T) { "Connection": "keep-alive, Upgrade", "Upgrade": "websocket", "Sec-WebSocket-Version": "13", - "Sec-WebSocket-Key": "meow123", + "Sec-WebSocket-Key": xrand.Base64(16), }, success: true, }, diff --git a/internal/test/xrand/xrand.go b/internal/test/xrand/xrand.go index 8de1ede8..82064d5c 100644 --- a/internal/test/xrand/xrand.go +++ b/internal/test/xrand/xrand.go @@ -2,6 +2,7 @@ package xrand import ( "crypto/rand" + "encoding/base64" "fmt" "math/big" "strings" @@ -45,3 +46,7 @@ func Int(max int) int { } return int(x.Int64()) } + +func Base64(n int) string { + return base64.StdEncoding.EncodeToString(Bytes(n)) +} From f46da9af6d363b6a5fe10d09705acd9d933af644 Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 20 Dec 2022 14:18:41 -0600 Subject: [PATCH 077/152] Remove build tag at top of files --- accept.go | 1 - accept_test.go | 1 - 2 files changed, 2 deletions(-) diff --git a/accept.go b/accept.go index 2f4fb2eb..24c5dca3 100644 --- a/accept.go +++ b/accept.go @@ -1,4 +1,3 @@ -//go:build !js // +build !js package websocket diff --git a/accept_test.go b/accept_test.go index d0cc4878..270f62da 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,4 +1,3 @@ -//go:build !js // +build !js package websocket From 3233cb5a0622a6eba869e3d169e98d11e1f2688f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 20 Dec 2022 14:27:27 -0600 Subject: [PATCH 078/152] Remove all leading and trailing whitespace --- accept.go | 3 +++ accept_test.go | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/accept.go b/accept.go index 24c5dca3..11b312d1 100644 --- a/accept.go +++ b/accept.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket @@ -185,6 +186,8 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ } websocketSecKey := r.Header.Get("Sec-WebSocket-Key") + // The RFC states to remove any leading or trailing whitespace. + websocketSecKey = strings.TrimSpace(websocketSecKey) if websocketSecKey == "" { return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } diff --git a/accept_test.go b/accept_test.go index 270f62da..ba245c47 100644 --- a/accept_test.go +++ b/accept_test.go @@ -1,3 +1,4 @@ +//go:build !js // +build !js package websocket @@ -229,6 +230,16 @@ func Test_verifyClientHandshake(t *testing.T) { }, success: true, }, + { + name: "successSecKeyExtraSpace", + h: map[string]string{ + "Connection": "keep-alive, Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + "Sec-WebSocket-Key": " " + xrand.Base64(16) + " ", + }, + success: true, + }, } for _, tc := range testCases { From 309e088c4f56983e20ae732aa59669f85144818f Mon Sep 17 00:00:00 2001 From: Steven Masley Date: Tue, 20 Dec 2022 14:46:50 -0600 Subject: [PATCH 079/152] Handle multiple sec-websocket-keys --- accept.go | 12 ++++++++---- accept_test.go | 22 +++++++++++++++++++++- 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/accept.go b/accept.go index 11b312d1..285b3103 100644 --- a/accept.go +++ b/accept.go @@ -185,13 +185,17 @@ func verifyClientRequest(w http.ResponseWriter, r *http.Request) (errCode int, _ return http.StatusBadRequest, fmt.Errorf("unsupported WebSocket protocol version (only 13 is supported): %q", r.Header.Get("Sec-WebSocket-Version")) } - websocketSecKey := r.Header.Get("Sec-WebSocket-Key") - // The RFC states to remove any leading or trailing whitespace. - websocketSecKey = strings.TrimSpace(websocketSecKey) - if websocketSecKey == "" { + websocketSecKeys := r.Header.Values("Sec-WebSocket-Key") + if len(websocketSecKeys) == 0 { return http.StatusBadRequest, errors.New("WebSocket protocol violation: missing Sec-WebSocket-Key") } + if len(websocketSecKeys) > 1 { + return http.StatusBadRequest, errors.New("WebSocket protocol violation: multiple Sec-WebSocket-Key headers") + } + + // The RFC states to remove any leading or trailing whitespace. + websocketSecKey := strings.TrimSpace(websocketSecKeys[0]) if v, err := base64.StdEncoding.DecodeString(websocketSecKey); err != nil || len(v) != 16 { return http.StatusBadRequest, fmt.Errorf("WebSocket protocol violation: invalid Sec-WebSocket-Key %q, must be a 16 byte base64 encoded string", websocketSecKey) } diff --git a/accept_test.go b/accept_test.go index ba245c47..5b37dfc8 100644 --- a/accept_test.go +++ b/accept_test.go @@ -185,6 +185,14 @@ func Test_verifyClientHandshake(t *testing.T) { }, { name: "missingWebSocketKey", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + }, + }, + { + name: "emptyWebSocketKey", h: map[string]string{ "Connection": "Upgrade", "Upgrade": "websocket", @@ -210,6 +218,18 @@ func Test_verifyClientHandshake(t *testing.T) { "Sec-WebSocket-Key": "notbase64", }, }, + { + name: "extraWebSocketKey", + h: map[string]string{ + "Connection": "Upgrade", + "Upgrade": "websocket", + "Sec-WebSocket-Version": "13", + // Kinda cheeky, but http headers are case-insensitive. + // If 2 sec keys are present, this is a failure condition. + "Sec-WebSocket-Key": xrand.Base64(16), + "sec-webSocket-key": xrand.Base64(16), + }, + }, { name: "badHTTPVersion", h: map[string]string{ @@ -256,7 +276,7 @@ func Test_verifyClientHandshake(t *testing.T) { } for k, v := range tc.h { - r.Header.Set(k, v) + r.Header.Add(k, v) } _, err := verifyClientRequest(httptest.NewRecorder(), r) From 305eab9a519e2d563636ea1ea7b0b82377acf2fb Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:31:59 -0700 Subject: [PATCH 080/152] misc: Format and compile #360 --- accept_test.go | 4 ++-- internal/test/xrand/xrand.go | 1 + 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/accept_test.go b/accept_test.go index 5b37dfc8..7cb85d0f 100644 --- a/accept_test.go +++ b/accept_test.go @@ -9,11 +9,11 @@ import ( "net" "net/http" "net/http/httptest" - "nhooyr.io/websocket/internal/test/xrand" "strings" "testing" "nhooyr.io/websocket/internal/test/assert" + "nhooyr.io/websocket/internal/test/xrand" ) func TestAccept(t *testing.T) { @@ -68,7 +68,7 @@ func TestAccept(t *testing.T) { r.Header.Set("Connection", "Upgrade") r.Header.Set("Upgrade", "websocket") r.Header.Set("Sec-WebSocket-Version", "13") - r.Header.Set("Sec-WebSocket-Key", "meow123") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) r.Header.Set("Sec-WebSocket-Extensions", extensions) return r } diff --git a/internal/test/xrand/xrand.go b/internal/test/xrand/xrand.go index 82064d5c..9bfb39ce 100644 --- a/internal/test/xrand/xrand.go +++ b/internal/test/xrand/xrand.go @@ -47,6 +47,7 @@ func Int(max int) int { return int(x.Int64()) } +// Base64 returns a randomly generated base64 string of length n. func Base64(n int) string { return base64.StdEncoding.EncodeToString(Bytes(n)) } From e361137d7e762ad4d58cd7ea244e052ba4fdb891 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:40:57 -0700 Subject: [PATCH 081/152] wsjs: Register OnError Closes #400 --- ws_js.go | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/ws_js.go b/ws_js.go index e60601e3..03919692 100644 --- a/ws_js.go +++ b/ws_js.go @@ -55,6 +55,7 @@ type Conn struct { closeWasClean bool releaseOnClose func() + releaseOnError func() releaseOnMessage func() readSignal chan struct{} @@ -92,9 +93,15 @@ func (c *Conn) init() { c.close(err, e.WasClean) c.releaseOnClose() + c.releaseOnError() c.releaseOnMessage() }) + c.releaseOnError = c.ws.OnError(func(v js.Value) { + c.setCloseErr(errors.New(v.Get("message").String())) + c.closeWithInternal() + }) + c.releaseOnMessage = c.ws.OnMessage(func(e wsjs.MessageEvent) { c.readBufMu.Lock() defer c.readBufMu.Unlock() From 8abed3a7c004a4f51453dd6f01fc881f0af07cf4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 21:54:08 -0700 Subject: [PATCH 082/152] close.go: Remove unnecessary log.Printf call --- close.go | 4 ---- 1 file changed, 4 deletions(-) diff --git a/close.go b/close.go index 24907c64..d78a5442 100644 --- a/close.go +++ b/close.go @@ -8,7 +8,6 @@ import ( "encoding/binary" "errors" "fmt" - "log" "time" "nhooyr.io/websocket/internal/errd" @@ -150,9 +149,6 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { var marshalErr error if ce.Code != StatusNoStatusRcvd { p, marshalErr = ce.bytes() - if marshalErr != nil { - log.Printf("websocket: %v", marshalErr) - } } writeErr := c.writeControl(context.Background(), opClose, p) From e4879ab74e5dd045a4ef5707f2c542b9cf4a4321 Mon Sep 17 00:00:00 2001 From: univerio Date: Thu, 25 May 2023 12:34:29 +0200 Subject: [PATCH 083/152] conn_test: Add TestConcurrentClosePing Updates #298 --- conn_test.go | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/conn_test.go b/conn_test.go index c814ca28..3df6c64a 100644 --- a/conn_test.go +++ b/conn_test.go @@ -553,3 +553,26 @@ func assertClose(tb testing.TB, c *websocket.Conn) { err := c.Close(websocket.StatusNormalClosure, "") assert.Success(tb, err) } + +func TestConcurrentClosePing(t *testing.T) { + t.Parallel() + for i := 0; i < 64; i++ { + func() { + c1, c2 := wstest.Pipe(nil, nil) + defer c1.CloseNow() + defer c2.CloseNow() + c1.CloseRead(context.Background()) + c2.CloseRead(context.Background()) + go func() { + for range time.Tick(time.Millisecond) { + if err := c1.Ping(context.Background()); err != nil { + return + } + } + }() + + time.Sleep(10 * time.Millisecond) + assert.Success(t, c1.Close(websocket.StatusNormalClosure, "")) + }() + } +} From 28c670953e8a6c6ecf147f89ac0085ac6510999e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 22:37:11 -0700 Subject: [PATCH 084/152] conn_test.go: Fix TestConcurrentClosePing Closes #298 Closes #394 The close frame was being received from the peer before we were able to reset our write timeout and so we thought the write kept failing but it never was... Thanks @univerio and @bhallionOhbibi --- read.go | 7 +++++-- write.go | 3 +++ 2 files changed, 8 insertions(+), 2 deletions(-) diff --git a/read.go b/read.go index bf4362df..72386088 100644 --- a/read.go +++ b/read.go @@ -62,9 +62,12 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { func (c *Conn) CloseRead(ctx context.Context) context.Context { ctx, cancel := context.WithCancel(ctx) go func() { + defer c.CloseNow() defer cancel() - c.Reader(ctx) - c.Close(StatusPolicyViolation, "unexpected data message") + _, _, err := c.Reader(ctx) + if err == nil { + c.Close(StatusPolicyViolation, "unexpected data message") + } }() return ctx } diff --git a/write.go b/write.go index b7cf6600..6747513d 100644 --- a/write.go +++ b/write.go @@ -323,6 +323,9 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco select { case <-c.closed: + if opcode == opClose { + return n, nil + } return n, errClosed case c.writeTimeout <- context.Background(): } From 6cec2ca22e36e702265fd0a9173be341c8e44397 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 22:47:59 -0700 Subject: [PATCH 085/152] close.go: Fix mid read close Closes #355 --- close.go | 7 +++++++ conn_test.go | 21 +++++++++++++++++++++ 2 files changed, 28 insertions(+) diff --git a/close.go b/close.go index d78a5442..fe1ced34 100644 --- a/close.go +++ b/close.go @@ -182,6 +182,13 @@ func (c *Conn) waitCloseHandshake() error { return c.readCloseFrameErr } + for i := int64(0); i < c.msgReader.payloadLength; i++ { + _, err := c.br.ReadByte() + if err != nil { + return err + } + } + for { h, err := c.readLoop(ctx) if err != nil { diff --git a/conn_test.go b/conn_test.go index 3df6c64a..abc1c81d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -308,6 +308,27 @@ func TestConn(t *testing.T) { assert.ErrorIs(t, websocket.ErrClosed, err1) assert.ErrorIs(t, websocket.ErrClosed, err2) }) + + t.Run("MidReadClose", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, nil, nil) + + tt.goEchoLoop(c2) + + c1.SetReadLimit(131072) + + for i := 0; i < 5; i++ { + err := wstest.Echo(tt.ctx, c1, 131072) + assert.Success(t, err) + } + + err := wsjson.Write(tt.ctx, c1, "four") + assert.Success(t, err) + _, _, err = c1.Reader(tt.ctx) + assert.Success(t, err) + + err = c1.Close(websocket.StatusNormalClosure, "") + assert.Success(t, err) + }) } func TestWasm(t *testing.T) { From 5fe95bbfc2939b32e43b94768be7b6a23f86cbc4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 22:50:56 -0700 Subject: [PATCH 086/152] write.go: Fix potential writeFrame deadlock Closes #405 You should always be reading from the connection with CloseRead so this shouldn't have affected anyone using the library correctly. --- write.go | 1 + 1 file changed, 1 insertion(+) diff --git a/write.go b/write.go index 6747513d..708d5a6a 100644 --- a/write.go +++ b/write.go @@ -280,6 +280,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco err = errClosed case <-ctx.Done(): err = ctx.Err() + default: } c.close(err) err = fmt.Errorf("failed to write frame: %w", err) From 308a8e26527cdb9c3ffc87bbdb299cd0d438fec4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 18 Oct 2023 23:21:29 -0700 Subject: [PATCH 087/152] autobahn_test.go: Fix TODOs --- autobahn_test.go | 53 ++++++++++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/autobahn_test.go b/autobahn_test.go index 41fae555..57ceebd5 100644 --- a/autobahn_test.go +++ b/autobahn_test.go @@ -6,6 +6,7 @@ package websocket_test import ( "context" "encoding/json" + "errors" "fmt" "io" "net" @@ -20,6 +21,7 @@ import ( "nhooyr.io/websocket/internal/errd" "nhooyr.io/websocket/internal/test/assert" "nhooyr.io/websocket/internal/test/wstest" + "nhooyr.io/websocket/internal/util" ) var excludedAutobahnCases = []string{ @@ -37,8 +39,7 @@ var autobahnCases = []string{"*"} // Used to run individual test cases. autobahnCases runs only those cases matched // and not excluded by excludedAutobahnCases. Adding cases here means excludedAutobahnCases // is niled. -// TODO: -// var forceAutobahnCases = []string{} +var onlyAutobahnCases = []string{} func TestAutobahn(t *testing.T) { t.Parallel() @@ -54,10 +55,15 @@ func TestAutobahn(t *testing.T) { ) } + if len(onlyAutobahnCases) > 0 { + excludedAutobahnCases = []string{} + autobahnCases = onlyAutobahnCases + } + ctx, cancel := context.WithTimeout(context.Background(), time.Hour) defer cancel() - wstestURL, closeFn, err := wstestServer(ctx) + wstestURL, closeFn, err := wstestServer(t, ctx) assert.Success(t, err) defer func() { assert.Success(t, closeFn()) @@ -90,7 +96,7 @@ func TestAutobahn(t *testing.T) { assert.Success(t, err) c.Close(websocket.StatusNormalClosure, "") - checkWSTestIndex(t, "./ci/out/wstestClientReports/index.json") + checkWSTestIndex(t, "./ci/out/autobahn-report/index.json") } func waitWS(ctx context.Context, url string) error { @@ -109,9 +115,7 @@ func waitWS(ctx context.Context, url string) error { return ctx.Err() } -// TODO: Let docker pick the port and use docker port to find it. -// Does mean we can't use -i but that's fine. -func wstestServer(ctx context.Context) (url string, closeFn func() error, err error) { +func wstestServer(tb testing.TB, ctx context.Context) (url string, closeFn func() error, err error) { defer errd.Wrap(&err, "failed to start autobahn wstest server") serverAddr, err := unusedListenAddr() @@ -124,7 +128,7 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er } url = "ws://" + serverAddr - const outDir = "ci/out/wstestClientReports" + const outDir = "ci/out/autobahn-report" specFile, err := tempJSONFile(map[string]interface{}{ "url": url, @@ -144,9 +148,15 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er }() dockerPull := exec.CommandContext(ctx, "docker", "pull", "crossbario/autobahn-testsuite") - // TODO: log to *testing.T - dockerPull.Stdout = os.Stdout - dockerPull.Stderr = os.Stderr + dockerPull.Stdout = util.WriterFunc(func(p []byte) (int, error) { + tb.Log(string(p)) + return len(p), nil + }) + dockerPull.Stderr = util.WriterFunc(func(p []byte) (int, error) { + tb.Log(string(p)) + return len(p), nil + }) + tb.Log(dockerPull) err = dockerPull.Run() if err != nil { return "", nil, fmt.Errorf("failed to pull docker image: %w", err) @@ -169,23 +179,32 @@ func wstestServer(ctx context.Context) (url string, closeFn func() error, err er // See https://github.com/crossbario/autobahn-testsuite/blob/058db3a36b7c3a1edf68c282307c6b899ca4857f/autobahntestsuite/autobahntestsuite/wstest.py#L124 "--webport=0", ) - fmt.Println(strings.Join(args, " ")) wstest := exec.CommandContext(ctx, "docker", args...) - // TODO: log to *testing.T - wstest.Stdout = os.Stdout - wstest.Stderr = os.Stderr + wstest.Stdout = util.WriterFunc(func(p []byte) (int, error) { + tb.Log(string(p)) + return len(p), nil + }) + wstest.Stderr = util.WriterFunc(func(p []byte) (int, error) { + tb.Log(string(p)) + return len(p), nil + }) + tb.Log(wstest) err = wstest.Start() if err != nil { return "", nil, fmt.Errorf("failed to start wstest: %w", err) } - // TODO: kill return url, func() error { err = wstest.Process.Kill() if err != nil { return fmt.Errorf("failed to kill wstest: %w", err) } - return nil + err = wstest.Wait() + var ee *exec.ExitError + if errors.As(err, &ee) && ee.ExitCode() == -1 { + return nil + } + return err }, nil } From d22d1f39eaacd5e6560e9d62c0d364477ff51604 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:23:29 -0700 Subject: [PATCH 088/152] ci/test.sh: Always benchmark --- .github/workflows/ci.yml | 9 +++++++++ .github/workflows/daily.yml | 4 ++-- ci/test.sh | 2 +- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8b88e81c..3c650580 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -36,3 +36,12 @@ jobs: with: name: coverage.html path: ./ci/out/coverage.html + + bench: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-go@v4 + with: + go-version-file: ./go.mod + - run: ./ci/bench.sh diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index b625fd68..b1e64fbc 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh -bench=. + - run: AUTOBAHN=1 ./ci/test.sh - uses: actions/upload-artifact@v3 with: name: coverage.html @@ -47,7 +47,7 @@ jobs: - uses: actions/setup-go@v4 with: go-version-file: ./go.mod - - run: AUTOBAHN=1 ./ci/test.sh -bench=. + - run: AUTOBAHN=1 ./ci/test.sh - uses: actions/upload-artifact@v3 with: name: coverage.html diff --git a/ci/test.sh b/ci/test.sh index 32bdcec1..eadfb9fe 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -3,7 +3,7 @@ set -eu cd -- "$(dirname "$0")/.." go install github.com/agnivade/wasmbrowsertest@latest -go test --race --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... +go test --race --bench=. --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... sed -i.bak '/stringer\.go/d' ci/out/coverage.prof sed -i.bak '/nhooyr.io\/websocket\/internal\/test/d' ci/out/coverage.prof sed -i.bak '/examples/d' ci/out/coverage.prof From 50952d771f238f37ad20bb69c1d8e7ea7cac4ee6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:24:06 -0700 Subject: [PATCH 089/152] compress.go: Rewrite compression docs --- compress.go | 50 +++++++++++++++++++++++++----------------------- compress_test.go | 27 ++++++++++++++++++++++++++ write.go | 2 +- 3 files changed, 54 insertions(+), 25 deletions(-) diff --git a/compress.go b/compress.go index 81de751b..d7a40d3b 100644 --- a/compress.go +++ b/compress.go @@ -9,43 +9,45 @@ import ( "sync" ) -// CompressionMode represents the modes available to the deflate extension. +// CompressionMode represents the modes available to the permessage-deflate extension. // See https://tools.ietf.org/html/rfc7692 // -// Works in all browsers except Safari which does not implement the deflate extension. +// Works in all modern browsers except Safari which does not implement the permessage-deflate extension. +// +// Compression is only used if the peer supports the mode selected. type CompressionMode int const ( - // CompressionDisabled disables the deflate extension. - // - // Use this if you are using a predominantly binary protocol with very - // little duplication in between messages or CPU and memory are more - // important than bandwidth. + // CompressionDisabled disables the negotiation of the permessage-deflate extension. // - // This is the default. + // This is the default. Do not enable compression without benchmarking for your particular use case first. CompressionDisabled CompressionMode = iota - // CompressionContextTakeover uses a 32 kB sliding window and flate.Writer per connection. - // It reuses the sliding window from previous messages. - // As most WebSocket protocols are repetitive, this can be very efficient. - // It carries an overhead of 32 kB + 1.2 MB for every connection compared to CompressionNoContextTakeover. + // CompressionNoContextTakeover compresses each message greater than 512 bytes. Each message is compressed with + // a new 1.2 MB flate.Writer pulled from a sync.Pool. Each message is read with a 40 KB flate.Reader pulled from + // a sync.Pool. // - // Sometime in the future it will carry 65 kB overhead instead once https://github.com/golang/go/issues/36919 - // is fixed. + // This means less efficient compression as the sliding window from previous messages will not be used but the + // memory overhead will be lower as there will be no fixed cost for the flate.Writer nor the 32 KB sliding window. + // Especially if the connections are long lived and seldom written to. // - // If the peer negotiates NoContextTakeover on the client or server side, it will be - // used instead as this is required by the RFC. - CompressionContextTakeover + // Thus, it uses less memory than CompressionContextTakeover but compresses less efficiently. + // + // If the peer does not support CompressionNoContextTakeover then we will fall back to CompressionDisabled. + CompressionNoContextTakeover - // CompressionNoContextTakeover grabs a new flate.Reader and flate.Writer as needed - // for every message. This applies to both server and client side. + // CompressionContextTakeover compresses each message greater than 128 bytes reusing the 32 KB sliding window from + // previous messages. i.e compression context across messages is preserved. // - // This means less efficient compression as the sliding window from previous messages - // will not be used but the memory overhead will be lower if the connections - // are long lived and seldom used. + // As most WebSocket protocols are text based and repetitive, this compression mode can be very efficient. // - // The message will only be compressed if greater than 512 bytes. - CompressionNoContextTakeover + // The memory overhead is a fixed 32 KB sliding window, a fixed 1.2 MB flate.Writer and a sync.Pool of 40 KB flate.Reader's + // that are used when reading and then returned. + // + // Thus, it uses more memory than CompressionNoContextTakeover but compresses more efficiently. + // + // If the peer does not support CompressionContextTakeover then we will fall back to CompressionNoContextTakeover. + CompressionContextTakeover ) func (m CompressionMode) opts() *compressionOptions { diff --git a/compress_test.go b/compress_test.go index 7b0e3a68..667e1408 100644 --- a/compress_test.go +++ b/compress_test.go @@ -4,6 +4,9 @@ package websocket import ( + "bytes" + "compress/flate" + "io" "strings" "testing" @@ -33,3 +36,27 @@ func Test_slidingWindow(t *testing.T) { }) } } + +func BenchmarkFlateWriter(b *testing.B) { + b.ReportAllocs() + for i := 0; i < b.N; i++ { + w, _ := flate.NewWriter(io.Discard, flate.BestSpeed) + // We have to write a byte to get the writer to allocate to its full extent. + w.Write([]byte{'a'}) + w.Flush() + } +} + +func BenchmarkFlateReader(b *testing.B) { + b.ReportAllocs() + + var buf bytes.Buffer + w, _ := flate.NewWriter(&buf, flate.BestSpeed) + w.Write([]byte{'a'}) + w.Flush() + + for i := 0; i < b.N; i++ { + r := flate.NewReader(bytes.NewReader(buf.Bytes())) + io.ReadAll(r) + } +} diff --git a/write.go b/write.go index 708d5a6a..a6a137d1 100644 --- a/write.go +++ b/write.go @@ -38,7 +38,7 @@ func (c *Conn) Writer(ctx context.Context, typ MessageType) (io.WriteCloser, err // // See the Writer method if you want to stream a message. // -// If compression is disabled or the threshold is not met, then it +// If compression is disabled or the compression threshold is not met, then it // will write the message in a single frame. func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { _, err := c.write(ctx, typ, p) From 9d9c9718e1e1a6822e7dfb51a38029416135838c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:37:16 -0700 Subject: [PATCH 090/152] Update docs --- README.md | 23 ++++++++++++----------- doc.go | 2 +- example_test.go | 14 +++++++------- internal/examples/chat/chat.go | 2 +- internal/examples/echo/server.go | 2 +- 5 files changed, 22 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f1a45972..5d2fa1c5 100644 --- a/README.md +++ b/README.md @@ -5,15 +5,9 @@ websocket is a minimal and idiomatic WebSocket library for Go. -> **note**: I haven't been responsive for questions/reports on the issue tracker but I do -> read through and there are no outstanding bugs. There are certainly some nice to haves -> that I should merge in/figure out but nothing critical. I haven't given up on adding new -> features and cleaning up the code further, just been busy. Should anything critical -> arise, I will fix it. - ## Install -```bash +```sh go get nhooyr.io/websocket ``` @@ -23,18 +17,23 @@ go get nhooyr.io/websocket - First class [context.Context](https://blog.golang.org/context) support - Fully passes the WebSocket [autobahn-testsuite](https://github.com/crossbario/autobahn-testsuite) - [Zero dependencies](https://pkg.go.dev/nhooyr.io/websocket?tab=imports) -- JSON and protobuf helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb) subpackages +- JSON helpers in the [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage - Zero alloc reads and writes - Concurrent writes - [Close handshake](https://pkg.go.dev/nhooyr.io/websocket#Conn.Close) - [net.Conn](https://pkg.go.dev/nhooyr.io/websocket#NetConn) wrapper - [Ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression +- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) ## Roadmap +- [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267) +- [ ] Graceful shutdown helper [#209](https://github.com/nhooyr/websocket/issues/209) +- [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) +- [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) ## Examples @@ -51,7 +50,7 @@ http.HandlerFunc(func (w http.ResponseWriter, r *http.Request) { if err != nil { // ... } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) defer cancel() @@ -78,7 +77,7 @@ c, _, err := websocket.Dial(ctx, "ws://localhost:8080", nil) if err != nil { // ... } -defer c.Close(websocket.StatusInternalError, "the sky is falling") +defer c.CloseNow() err = wsjson.Write(ctx, c, "hi") if err != nil { @@ -110,12 +109,14 @@ Advantages of nhooyr.io/websocket: - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) +- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) -- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) and [wspb](https://pkg.go.dev/nhooyr.io/websocket/wspb) subpackages +- Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). + Soon we'll have assembly and be 4.5x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) diff --git a/doc.go b/doc.go index a2b873c7..ea38aa34 100644 --- a/doc.go +++ b/doc.go @@ -13,7 +13,7 @@ // // The examples are the best way to understand how to correctly use the library. // -// The wsjson and wspb subpackages contain helpers for JSON and protobuf messages. +// The wsjson subpackage contain helpers for JSON and protobuf messages. // // More documentation at https://nhooyr.io/websocket. // diff --git a/example_test.go b/example_test.go index 2e55eb96..590c0411 100644 --- a/example_test.go +++ b/example_test.go @@ -20,7 +20,7 @@ func ExampleAccept() { log.Println(err) return } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() ctx, cancel := context.WithTimeout(r.Context(), time.Second*10) defer cancel() @@ -50,7 +50,7 @@ func ExampleDial() { if err != nil { log.Fatal(err) } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() err = wsjson.Write(ctx, c, "hi") if err != nil { @@ -71,7 +71,7 @@ func ExampleCloseStatus() { if err != nil { log.Fatal(err) } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() _, _, err = c.Reader(ctx) if websocket.CloseStatus(err) != websocket.StatusNormalClosure { @@ -88,7 +88,7 @@ func Example_writeOnly() { log.Println(err) return } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() ctx, cancel := context.WithTimeout(r.Context(), time.Minute*10) defer cancel() @@ -145,7 +145,7 @@ func ExampleConn_Ping() { if err != nil { log.Fatal(err) } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() // Required to read the Pongs from the server. ctx = c.CloseRead(ctx) @@ -162,10 +162,10 @@ func ExampleConn_Ping() { // This example demonstrates full stack chat with an automated test. func Example_fullStackChat() { - // https://github.com/nhooyr/websocket/tree/master/examples/chat + // https://github.com/nhooyr/websocket/tree/master/internal/examples/chat } // This example demonstrates a echo server. func Example_echo() { - // https://github.com/nhooyr/websocket/tree/master/examples/echo + // https://github.com/nhooyr/websocket/tree/master/internal/examples/echo } diff --git a/internal/examples/chat/chat.go b/internal/examples/chat/chat.go index 9d393d87..78a5696a 100644 --- a/internal/examples/chat/chat.go +++ b/internal/examples/chat/chat.go @@ -74,7 +74,7 @@ func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { cs.logf("%v", err) return } - defer c.Close(websocket.StatusInternalError, "") + defer c.CloseNow() err = cs.subscribe(r.Context(), c) if errors.Is(err, context.Canceled) { diff --git a/internal/examples/echo/server.go b/internal/examples/echo/server.go index e9f70f03..246ad582 100644 --- a/internal/examples/echo/server.go +++ b/internal/examples/echo/server.go @@ -28,7 +28,7 @@ func (s echoServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.logf("%v", err) return } - defer c.Close(websocket.StatusInternalError, "the sky is falling") + defer c.CloseNow() if c.Subprotocol() != "echo" { c.Close(websocket.StatusPolicyViolation, "client must speak the echo subprotocol") From 25a5ca47d8d9c5edd0519f1c46d0bf1e685014a0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:54:49 -0700 Subject: [PATCH 091/152] netconn.go: Fix panic on zero or negative deadline durations Glad no one ran into this in production. --- conn_test.go | 12 ++++++++++++ netconn.go | 12 ++++++++++-- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/conn_test.go b/conn_test.go index abc1c81d..17c52c32 100644 --- a/conn_test.go +++ b/conn_test.go @@ -236,6 +236,18 @@ func TestConn(t *testing.T) { assert.Equal(t, "read msg", s, string(b)) }) + t.Run("netConn/pastDeadline", func(t *testing.T) { + tt, c1, c2 := newConnTest(t, nil, nil) + + n1 := websocket.NetConn(tt.ctx, c1, websocket.MessageBinary) + n2 := websocket.NetConn(tt.ctx, c2, websocket.MessageBinary) + + n1.SetDeadline(time.Now().Add(-time.Minute)) + n2.SetDeadline(time.Now().Add(-time.Minute)) + + // No panic we're good. + }) + t.Run("wsjson", func(t *testing.T) { tt, c1, c2 := newConnTest(t, nil, nil) diff --git a/netconn.go b/netconn.go index e398b4f7..1667f45c 100644 --- a/netconn.go +++ b/netconn.go @@ -210,7 +210,11 @@ func (nc *netConn) SetWriteDeadline(t time.Time) error { if t.IsZero() { nc.writeTimer.Stop() } else { - nc.writeTimer.Reset(time.Until(t)) + dur := time.Until(t) + if dur <= 0 { + dur = 1 + } + nc.writeTimer.Reset(dur) } return nil } @@ -220,7 +224,11 @@ func (nc *netConn) SetReadDeadline(t time.Time) error { if t.IsZero() { nc.readTimer.Stop() } else { - nc.readTimer.Reset(time.Until(t)) + dur := time.Until(t) + if dur <= 0 { + dur = 1 + } + nc.readTimer.Reset(dur) } return nil } From cdeb9806656bd144fa49dcac6e717e88d6e919f0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:55:44 -0700 Subject: [PATCH 092/152] ws_js.go: Add CloseNow --- doc.go | 1 + ws_js.go | 23 ++++++++++++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/doc.go b/doc.go index ea38aa34..2ab648a6 100644 --- a/doc.go +++ b/doc.go @@ -28,6 +28,7 @@ // // - Accept always errors out // - Conn.Ping is no-op +// - Conn.CloseNow is Close(StatusGoingAway, "") // - HTTPClient, HTTPHeader and CompressionMode in DialOptions are no-op // - *http.Response from Dial is &http.Response{} with a 101 status code on success package websocket // import "nhooyr.io/websocket" diff --git a/ws_js.go b/ws_js.go index 03919692..59bb685c 100644 --- a/ws_js.go +++ b/ws_js.go @@ -151,7 +151,7 @@ func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { return 0, nil, ctx.Err() case <-c.readSignal: case <-c.closed: - return 0, nil, c.closeErr + return 0, nil, errClosed } c.readBufMu.Lock() @@ -205,7 +205,7 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { if c.isClosed() { - return c.closeErr + return errClosed } switch typ { case MessageBinary: @@ -229,19 +229,28 @@ func (c *Conn) Close(code StatusCode, reason string) error { return nil } +// CloseNow closes the WebSocket connection without attempting a close handshake. +// Use When you do not want the overhead of the close handshake. +// +// note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close +// a WebSocket without the close handshake. +func (c *Conn) CloseNow() error { + return c.Close(StatusGoingAway, "") +} + func (c *Conn) exportedClose(code StatusCode, reason string) error { c.closingMu.Lock() defer c.closingMu.Unlock() + if c.isClosed() { + return errClosed + } + ce := fmt.Errorf("sent close: %w", CloseError{ Code: code, Reason: reason, }) - if c.isClosed() { - return fmt.Errorf("tried to close with %q but connection already closed: %w", ce, c.closeErr) - } - c.setCloseErr(ce) err := c.ws.Close(int(code), reason) if err != nil { @@ -312,7 +321,7 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp StatusCode: http.StatusSwitchingProtocols, }, nil case <-c.closed: - return nil, nil, c.closeErr + return nil, nil, errClosed } } From fb3b083efa5e72d35844426f20c8cfcdec00a57d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 00:56:56 -0700 Subject: [PATCH 093/152] close.go: Drop support for Go 1.13 --- close.go | 7 ++++--- close_go113.go | 9 --------- close_go116.go | 9 --------- conn.go | 7 ++++--- export_test.go | 8 ++++++-- read.go | 13 +++++++------ write.go | 9 +++++---- ws_js.go | 9 +++++---- 8 files changed, 31 insertions(+), 40 deletions(-) delete mode 100644 close_go113.go delete mode 100644 close_go116.go diff --git a/close.go b/close.go index fe1ced34..0abc864f 100644 --- a/close.go +++ b/close.go @@ -8,6 +8,7 @@ import ( "encoding/binary" "errors" "fmt" + "net" "time" "nhooyr.io/websocket/internal/errd" @@ -107,7 +108,7 @@ func (c *Conn) CloseNow() (err error) { defer errd.Wrap(&err, "failed to close WebSocket") if c.isClosed() { - return errClosed + return net.ErrClosed } c.close(nil) @@ -124,7 +125,7 @@ func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { return writeErr } - if CloseStatus(closeHandshakeErr) == -1 && !errors.Is(errClosed, closeHandshakeErr) { + if CloseStatus(closeHandshakeErr) == -1 && !errors.Is(net.ErrClosed, closeHandshakeErr) { return closeHandshakeErr } @@ -137,7 +138,7 @@ func (c *Conn) writeClose(code StatusCode, reason string) error { c.wroteClose = true c.closeMu.Unlock() if wroteClose { - return errClosed + return net.ErrClosed } ce := CloseError{ diff --git a/close_go113.go b/close_go113.go deleted file mode 100644 index caf1b89e..00000000 --- a/close_go113.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build !go1.16 && !js - -package websocket - -import ( - "errors" -) - -var errClosed = errors.New("use of closed network connection") diff --git a/close_go116.go b/close_go116.go deleted file mode 100644 index 9d986109..00000000 --- a/close_go116.go +++ /dev/null @@ -1,9 +0,0 @@ -//go:build go1.16 && !js - -package websocket - -import ( - "net" -) - -var errClosed = net.ErrClosed diff --git a/conn.go b/conn.go index 36662a93..3b3a9f98 100644 --- a/conn.go +++ b/conn.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "net" "runtime" "strconv" "sync" @@ -228,7 +229,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { select { case <-c.closed: - return errClosed + return net.ErrClosed case <-ctx.Done(): err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) c.close(err) @@ -266,7 +267,7 @@ func (m *mu) tryLock() bool { func (m *mu) lock(ctx context.Context) error { select { case <-m.c.closed: - return errClosed + return net.ErrClosed case <-ctx.Done(): err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) m.c.close(err) @@ -279,7 +280,7 @@ func (m *mu) lock(ctx context.Context) error { case <-m.c.closed: // Make sure to release. m.unlock() - return errClosed + return net.ErrClosed default: } return nil diff --git a/export_test.go b/export_test.go index e322c36f..a644d8f0 100644 --- a/export_test.go +++ b/export_test.go @@ -3,7 +3,11 @@ package websocket -import "nhooyr.io/websocket/internal/util" +import ( + "net" + + "nhooyr.io/websocket/internal/util" +) func (c *Conn) RecordBytesWritten() *int { var bytesWritten int @@ -24,7 +28,7 @@ func (c *Conn) RecordBytesRead() *int { return &bytesRead } -var ErrClosed = errClosed +var ErrClosed = net.ErrClosed var ExportedDial = dial var SecWebSocketAccept = secWebSocketAccept diff --git a/read.go b/read.go index 72386088..9ab28812 100644 --- a/read.go +++ b/read.go @@ -9,6 +9,7 @@ import ( "errors" "fmt" "io" + "net" "strings" "time" @@ -206,7 +207,7 @@ func (c *Conn) readLoop(ctx context.Context) (header, error) { func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { case <-c.closed: - return header{}, errClosed + return header{}, net.ErrClosed case c.readTimeout <- ctx: } @@ -214,7 +215,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { if err != nil { select { case <-c.closed: - return header{}, errClosed + return header{}, net.ErrClosed case <-ctx.Done(): return header{}, ctx.Err() default: @@ -225,7 +226,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { select { case <-c.closed: - return header{}, errClosed + return header{}, net.ErrClosed case c.readTimeout <- context.Background(): } @@ -235,7 +236,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { case <-c.closed: - return 0, errClosed + return 0, net.ErrClosed case c.readTimeout <- ctx: } @@ -243,7 +244,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { if err != nil { select { case <-c.closed: - return n, errClosed + return n, net.ErrClosed case <-ctx.Done(): return n, ctx.Err() default: @@ -255,7 +256,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { select { case <-c.closed: - return n, errClosed + return n, net.ErrClosed case c.readTimeout <- context.Background(): } diff --git a/write.go b/write.go index a6a137d1..3d062656 100644 --- a/write.go +++ b/write.go @@ -11,6 +11,7 @@ import ( "errors" "fmt" "io" + "net" "time" "compress/flate" @@ -262,14 +263,14 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco case <-ctx.Done(): return 0, ctx.Err() case <-c.closed: - return 0, errClosed + return 0, net.ErrClosed } } defer c.writeFrameMu.unlock() select { case <-c.closed: - return 0, errClosed + return 0, net.ErrClosed case c.writeTimeout <- ctx: } @@ -277,7 +278,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if err != nil { select { case <-c.closed: - err = errClosed + err = net.ErrClosed case <-ctx.Done(): err = ctx.Err() default: @@ -327,7 +328,7 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco if opcode == opClose { return n, nil } - return n, errClosed + return n, net.ErrClosed case c.writeTimeout <- context.Background(): } diff --git a/ws_js.go b/ws_js.go index 59bb685c..cae68bb6 100644 --- a/ws_js.go +++ b/ws_js.go @@ -6,6 +6,7 @@ import ( "errors" "fmt" "io" + "net" "net/http" "reflect" "runtime" @@ -151,7 +152,7 @@ func (c *Conn) read(ctx context.Context) (MessageType, []byte, error) { return 0, nil, ctx.Err() case <-c.readSignal: case <-c.closed: - return 0, nil, errClosed + return 0, nil, net.ErrClosed } c.readBufMu.Lock() @@ -205,7 +206,7 @@ func (c *Conn) Write(ctx context.Context, typ MessageType, p []byte) error { func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { if c.isClosed() { - return errClosed + return net.ErrClosed } switch typ { case MessageBinary: @@ -243,7 +244,7 @@ func (c *Conn) exportedClose(code StatusCode, reason string) error { defer c.closingMu.Unlock() if c.isClosed() { - return errClosed + return net.ErrClosed } ce := fmt.Errorf("sent close: %w", CloseError{ @@ -321,7 +322,7 @@ func dial(ctx context.Context, url string, opts *DialOptions) (*Conn, *http.Resp StatusCode: http.StatusSwitchingProtocols, }, nil case <-c.closed: - return nil, nil, errClosed + return nil, nil, net.ErrClosed } } From 9fdcb5d7dd378874db50414680094c7443cb007a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 01:07:11 -0700 Subject: [PATCH 094/152] Misc fixes --- README.md | 2 +- ci/test.sh | 18 +++++++++--------- compress.go | 26 +++++++++++++------------- make.sh | 4 ++++ 4 files changed, 27 insertions(+), 23 deletions(-) diff --git a/README.md b/README.md index 5d2fa1c5..40921d41 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-86%25-success)](https://nhooyrio-websocket-coverage.netlify.app) +[![coverage](https://img.shields.io/badge/coverage-89%25-success)](https://nhooyr.io/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. diff --git a/ci/test.sh b/ci/test.sh index eadfb9fe..83bb9832 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -2,6 +2,15 @@ set -eu cd -- "$(dirname "$0")/.." +( + cd ./internal/examples + go test "$@" ./... +) +( + cd ./internal/thirdparty + go test "$@" ./... +) + go install github.com/agnivade/wasmbrowsertest@latest go test --race --bench=. --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... sed -i.bak '/stringer\.go/d' ci/out/coverage.prof @@ -12,12 +21,3 @@ sed -i.bak '/examples/d' ci/out/coverage.prof go tool cover -func ci/out/coverage.prof | tail -n1 go tool cover -html=ci/out/coverage.prof -o=ci/out/coverage.html - -( - cd ./internal/examples - go test "$@" ./... -) -( - cd ./internal/thirdparty - go test "$@" ./... -) diff --git a/compress.go b/compress.go index d7a40d3b..1f3adcfb 100644 --- a/compress.go +++ b/compress.go @@ -23,19 +23,6 @@ const ( // This is the default. Do not enable compression without benchmarking for your particular use case first. CompressionDisabled CompressionMode = iota - // CompressionNoContextTakeover compresses each message greater than 512 bytes. Each message is compressed with - // a new 1.2 MB flate.Writer pulled from a sync.Pool. Each message is read with a 40 KB flate.Reader pulled from - // a sync.Pool. - // - // This means less efficient compression as the sliding window from previous messages will not be used but the - // memory overhead will be lower as there will be no fixed cost for the flate.Writer nor the 32 KB sliding window. - // Especially if the connections are long lived and seldom written to. - // - // Thus, it uses less memory than CompressionContextTakeover but compresses less efficiently. - // - // If the peer does not support CompressionNoContextTakeover then we will fall back to CompressionDisabled. - CompressionNoContextTakeover - // CompressionContextTakeover compresses each message greater than 128 bytes reusing the 32 KB sliding window from // previous messages. i.e compression context across messages is preserved. // @@ -48,6 +35,19 @@ const ( // // If the peer does not support CompressionContextTakeover then we will fall back to CompressionNoContextTakeover. CompressionContextTakeover + + // CompressionNoContextTakeover compresses each message greater than 512 bytes. Each message is compressed with + // a new 1.2 MB flate.Writer pulled from a sync.Pool. Each message is read with a 40 KB flate.Reader pulled from + // a sync.Pool. + // + // This means less efficient compression as the sliding window from previous messages will not be used but the + // memory overhead will be lower as there will be no fixed cost for the flate.Writer nor the 32 KB sliding window. + // Especially if the connections are long lived and seldom written to. + // + // Thus, it uses less memory than CompressionContextTakeover but compresses less efficiently. + // + // If the peer does not support CompressionNoContextTakeover then we will fall back to CompressionDisabled. + CompressionNoContextTakeover ) func (m CompressionMode) opts() *compressionOptions { diff --git a/make.sh b/make.sh index 81909d72..170d00a8 100755 --- a/make.sh +++ b/make.sh @@ -2,7 +2,11 @@ set -eu cd -- "$(dirname "$0")" +echo "=== fmt.sh" ./ci/fmt.sh +echo "=== lint.sh" ./ci/lint.sh +echo "=== test.sh" ./ci/test.sh "$@" +echo "=== bench.sh" ./ci/bench.sh From db79f72fc2efc3f1347fa61c7f0ecc5f3fdd47b0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 01:20:57 -0700 Subject: [PATCH 095/152] Update README.md --- README.md | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 40921d41..ba935586 100644 --- a/README.md +++ b/README.md @@ -29,9 +29,15 @@ go get nhooyr.io/websocket ## Roadmap +See GitHub issues for minor issues but the major future enhancements are: + +- [ ] Perfect examples [#217](https://github.com/nhooyr/websocket/issues/217) +- [ ] wstest.Pipe for in memory testing [#340](https://github.com/nhooyr/websocket/issues/340) - [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267) -- [ ] Graceful shutdown helper [#209](https://github.com/nhooyr/websocket/issues/209) +- [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246) +- [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209) - [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) + - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) From 108d137e4ce60d187d27c2455f6b056933ee83a8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 01:37:27 -0700 Subject: [PATCH 096/152] Update README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index ba935586..8850f511 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket [![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-89%25-success)](https://nhooyr.io/websocket/coverage.html) +[![coverage](https://img.shields.io/badge/coverage-91%25-success)](https://nhooyr.io/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. @@ -126,7 +126,6 @@ Advantages of nhooyr.io/websocket: - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) -- Actively maintained ([gorilla/websocket#370](https://github.com/gorilla/websocket/issues/370)) #### golang.org/x/net/websocket From e6a7e0e8e6fe579058a23bef78e03a17172d6ed6 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 01:56:06 -0700 Subject: [PATCH 097/152] main_test.go: Add to detect goroutine leaks Updates #330 --- main_test.go | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 main_test.go diff --git a/main_test.go b/main_test.go new file mode 100644 index 00000000..336be71c --- /dev/null +++ b/main_test.go @@ -0,0 +1,17 @@ +package websocket_test + +import ( + "fmt" + "os" + "runtime" + "testing" +) + +func TestMain(m *testing.M) { + code := m.Run() + if runtime.NumGoroutine() != 1 { + fmt.Fprintf(os.Stderr, "goroutine leak detected, expected 1 but got %d goroutines\n", runtime.NumGoroutine()) + os.Exit(1) + } + os.Exit(code) +} From 6ed989afc10be2cf8139362ca006cad4a1cb98d8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 02:51:36 -0700 Subject: [PATCH 098/152] Ensure no goroutines leak after Close Closes #330 --- conn.go | 34 ++++++++++++++++++++++++---------- conn_test.go | 17 +++++++++-------- dial_test.go | 3 ++- main_test.go | 15 ++++++++++++++- read.go | 5 +++++ write.go | 25 +++++++++++++++---------- ws_js.go | 2 +- 7 files changed, 70 insertions(+), 31 deletions(-) diff --git a/conn.go b/conn.go index 3b3a9f98..5084dce1 100644 --- a/conn.go +++ b/conn.go @@ -53,8 +53,10 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer - readTimeout chan context.Context - writeTimeout chan context.Context + timeoutLoopCancel context.CancelFunc + timeoutLoopDone chan struct{} + readTimeout chan context.Context + writeTimeout chan context.Context // Read state. readMu *mu @@ -102,8 +104,9 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), + timeoutLoopDone: make(chan struct{}), + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), @@ -130,7 +133,9 @@ func newConn(cfg connConfig) *Conn { c.close(errors.New("connection garbage collected")) }) - go c.timeoutLoop() + var ctx context.Context + ctx, c.timeoutLoopCancel = context.WithCancel(context.Background()) + go c.timeoutLoop(ctx) return c } @@ -152,6 +157,10 @@ func (c *Conn) close(err error) { err = c.rwc.Close() } c.setCloseErrLocked(err) + + c.timeoutLoopCancel() + <-c.timeoutLoopDone + close(c.closed) runtime.SetFinalizer(c, nil) @@ -160,18 +169,23 @@ func (c *Conn) close(err error) { // closeErr. c.rwc.Close() - go func() { - c.msgWriter.close() - c.msgReader.close() - }() + c.closeMu.Unlock() + defer c.closeMu.Lock() + + c.msgWriter.close() + c.msgReader.close() } -func (c *Conn) timeoutLoop() { +func (c *Conn) timeoutLoop(ctx context.Context) { + defer close(c.timeoutLoopDone) + readCtx := context.Background() writeCtx := context.Background() for { select { + case <-ctx.Done(): + return case <-c.closed: return diff --git a/conn_test.go b/conn_test.go index 17c52c32..97b172dc 100644 --- a/conn_test.go +++ b/conn_test.go @@ -399,10 +399,8 @@ func newConnTest(t testing.TB, dialOpts *websocket.DialOptions, acceptOpts *webs c1, c2 = c2, c1 } t.Cleanup(func() { - // We don't actually care whether this succeeds so we just run it in a separate goroutine to avoid - // blocking the test shutting down. - go c2.Close(websocket.StatusInternalError, "") - go c1.Close(websocket.StatusInternalError, "") + c2.CloseNow() + c1.CloseNow() }) return tt, c1, c2 @@ -596,16 +594,19 @@ func TestConcurrentClosePing(t *testing.T) { defer c2.CloseNow() c1.CloseRead(context.Background()) c2.CloseRead(context.Background()) - go func() { + errc := xsync.Go(func() error { for range time.Tick(time.Millisecond) { - if err := c1.Ping(context.Background()); err != nil { - return + err := c1.Ping(context.Background()) + if err != nil { + return err } } - }() + panic("unreachable") + }) time.Sleep(10 * time.Millisecond) assert.Success(t, c1.Close(websocket.StatusNormalClosure, "")) + <-errc }() } } diff --git a/dial_test.go b/dial_test.go index 63cb4be6..237a2874 100644 --- a/dial_test.go +++ b/dial_test.go @@ -164,11 +164,12 @@ func Test_verifyHostOverride(t *testing.T) { }, nil } - _, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ + c, _, err := websocket.Dial(ctx, "ws://example.com", &websocket.DialOptions{ HTTPClient: mockHTTPClient(rt), Host: tc.host, }) assert.Success(t, err) + c.CloseNow() }) } diff --git a/main_test.go b/main_test.go index 336be71c..2b93bb18 100644 --- a/main_test.go +++ b/main_test.go @@ -7,10 +7,23 @@ import ( "testing" ) +func goroutineStacks() []byte { + buf := make([]byte, 512) + for { + m := runtime.Stack(buf, true) + if m < len(buf) { + return buf[:m] + } + buf = make([]byte, len(buf)*2) + } +} + func TestMain(m *testing.M) { code := m.Run() - if runtime.NumGoroutine() != 1 { + if runtime.GOOS != "js" && runtime.NumGoroutine() != 1 || + runtime.GOOS == "js" && runtime.NumGoroutine() != 2 { fmt.Fprintf(os.Stderr, "goroutine leak detected, expected 1 but got %d goroutines\n", runtime.NumGoroutine()) + fmt.Fprintf(os.Stderr, "%s\n", goroutineStacks()) os.Exit(1) } os.Exit(code) diff --git a/read.go b/read.go index 9ab28812..5c180fba 100644 --- a/read.go +++ b/read.go @@ -219,6 +219,7 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: + c.readMu.unlock() c.close(err) return header{}, err } @@ -249,6 +250,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { return n, ctx.Err() default: err = fmt.Errorf("failed to read frame payload: %w", err) + c.readMu.unlock() c.close(err) return n, err } @@ -319,6 +321,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { err = fmt.Errorf("received close frame: %w", ce) c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) + c.readMu.unlock() c.close(err) return err } @@ -334,6 +337,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro if !c.msgReader.fin { err = errors.New("previous message not read to completion") + c.readMu.unlock() c.close(fmt.Errorf("failed to get reader: %w", err)) return 0, nil, err } @@ -409,6 +413,7 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { } if err != nil { err = fmt.Errorf("failed to read: %w", err) + mr.c.readMu.unlock() mr.c.close(err) } return n, err diff --git a/write.go b/write.go index 3d062656..0fbfd9cd 100644 --- a/write.go +++ b/write.go @@ -109,7 +109,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error if !c.flate() { defer c.msgWriter.mu.unlock() - return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) + return c.writeFrame(true, ctx, true, false, c.msgWriter.opcode, p) } n, err := mw.Write(p) @@ -159,6 +159,7 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer func() { if err != nil { err = fmt.Errorf("failed to write: %w", err) + mw.writeMu.unlock() mw.c.close(err) } }() @@ -179,7 +180,7 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { } func (mw *msgWriter) write(p []byte) (int, error) { - n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) + n, err := mw.c.writeFrame(true, mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { return n, fmt.Errorf("failed to write data frame: %w", err) } @@ -191,17 +192,17 @@ func (mw *msgWriter) write(p []byte) (int, error) { func (mw *msgWriter) Close() (err error) { defer errd.Wrap(&err, "failed to close writer") - if mw.closed { - return errors.New("writer already closed") - } - mw.closed = true - err = mw.writeMu.lock(mw.ctx) if err != nil { return err } defer mw.writeMu.unlock() + if mw.closed { + return errors.New("writer already closed") + } + mw.closed = true + if mw.flate { err = mw.flateWriter.Flush() if err != nil { @@ -209,7 +210,7 @@ func (mw *msgWriter) Close() (err error) { } } - _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) + _, err = mw.c.writeFrame(true, mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return fmt.Errorf("failed to write fin frame: %w", err) } @@ -235,7 +236,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - _, err := c.writeFrame(ctx, true, false, opcode, p) + _, err := c.writeFrame(false, ctx, true, false, opcode, p) if err != nil { return fmt.Errorf("failed to write control frame %v: %w", opcode, err) } @@ -243,7 +244,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error } // frame handles all writes to the connection. -func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { +func (c *Conn) writeFrame(msgWriter bool, ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { err = c.writeFrameMu.lock(ctx) if err != nil { return 0, err @@ -283,6 +284,10 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco err = ctx.Err() default: } + c.writeFrameMu.unlock() + if msgWriter { + c.msgWriter.writeMu.unlock() + } c.close(err) err = fmt.Errorf("failed to write frame: %w", err) } diff --git a/ws_js.go b/ws_js.go index cae68bb6..180d0564 100644 --- a/ws_js.go +++ b/ws_js.go @@ -231,7 +231,7 @@ func (c *Conn) Close(code StatusCode, reason string) error { } // CloseNow closes the WebSocket connection without attempting a close handshake. -// Use When you do not want the overhead of the close handshake. +// Use when you do not want the overhead of the close handshake. // // note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close // a WebSocket without the close handshake. From d7a55cff33db1eebcd8eb4dcb42cb736b24d46a9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 03:14:35 -0700 Subject: [PATCH 099/152] Ensure no goroutines leak after Close in a cleaner way Closes #330 --- close.go | 4 +++- conn.go | 51 +++++++++++++++++++++++++++------------------------ read.go | 8 +++----- write.go | 23 +++++++++-------------- 4 files changed, 42 insertions(+), 44 deletions(-) diff --git a/close.go b/close.go index 0abc864f..1053751c 100644 --- a/close.go +++ b/close.go @@ -99,12 +99,14 @@ func CloseStatus(err error) StatusCode { // Close will unblock all goroutines interacting with the connection once // complete. func (c *Conn) Close(code StatusCode, reason string) error { + defer c.wgWait() return c.closeHandshake(code, reason) } // CloseNow closes the WebSocket connection without attempting a close handshake. -// Use When you do not want the overhead of the close handshake. +// Use when you do not want the overhead of the close handshake. func (c *Conn) CloseNow() (err error) { + defer c.wgWait() defer errd.Wrap(&err, "failed to close WebSocket") if c.isClosed() { diff --git a/conn.go b/conn.go index 5084dce1..05531c3b 100644 --- a/conn.go +++ b/conn.go @@ -45,6 +45,8 @@ const ( type Conn struct { noCopy + wg sync.WaitGroup + subprotocol string rwc io.ReadWriteCloser client bool @@ -53,10 +55,8 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer - timeoutLoopCancel context.CancelFunc - timeoutLoopDone chan struct{} - readTimeout chan context.Context - writeTimeout chan context.Context + readTimeout chan context.Context + writeTimeout chan context.Context // Read state. readMu *mu @@ -104,9 +104,8 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - timeoutLoopDone: make(chan struct{}), - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), @@ -133,9 +132,7 @@ func newConn(cfg connConfig) *Conn { c.close(errors.New("connection garbage collected")) }) - var ctx context.Context - ctx, c.timeoutLoopCancel = context.WithCancel(context.Background()) - go c.timeoutLoop(ctx) + c.wgGo(c.timeoutLoop) return c } @@ -158,9 +155,6 @@ func (c *Conn) close(err error) { } c.setCloseErrLocked(err) - c.timeoutLoopCancel() - <-c.timeoutLoopDone - close(c.closed) runtime.SetFinalizer(c, nil) @@ -169,23 +163,18 @@ func (c *Conn) close(err error) { // closeErr. c.rwc.Close() - c.closeMu.Unlock() - defer c.closeMu.Lock() - - c.msgWriter.close() - c.msgReader.close() + c.wgGo(func() { + c.msgWriter.close() + c.msgReader.close() + }) } -func (c *Conn) timeoutLoop(ctx context.Context) { - defer close(c.timeoutLoopDone) - +func (c *Conn) timeoutLoop() { readCtx := context.Background() writeCtx := context.Background() for { select { - case <-ctx.Done(): - return case <-c.closed: return @@ -194,7 +183,9 @@ func (c *Conn) timeoutLoop(ctx context.Context) { case <-readCtx.Done(): c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - go c.writeError(StatusPolicyViolation, errors.New("timed out")) + c.wgGo(func() { + c.writeError(StatusPolicyViolation, errors.New("read timed out")) + }) case <-writeCtx.Done(): c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) return @@ -311,3 +302,15 @@ func (m *mu) unlock() { type noCopy struct{} func (*noCopy) Lock() {} + +func (c *Conn) wgGo(fn func()) { + c.wg.Add(1) + go func() { + defer c.wg.Done() + fn() + }() +} + +func (c *Conn) wgWait() { + c.wg.Wait() +} diff --git a/read.go b/read.go index 5c180fba..8742842e 100644 --- a/read.go +++ b/read.go @@ -62,8 +62,11 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { // frames are responded to. This means c.Ping and c.Close will still work as expected. func (c *Conn) CloseRead(ctx context.Context) context.Context { ctx, cancel := context.WithCancel(ctx) + + c.wg.Add(1) go func() { defer c.CloseNow() + defer c.wg.Done() defer cancel() _, _, err := c.Reader(ctx) if err == nil { @@ -219,7 +222,6 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: - c.readMu.unlock() c.close(err) return header{}, err } @@ -250,7 +252,6 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { return n, ctx.Err() default: err = fmt.Errorf("failed to read frame payload: %w", err) - c.readMu.unlock() c.close(err) return n, err } @@ -321,7 +322,6 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { err = fmt.Errorf("received close frame: %w", ce) c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) - c.readMu.unlock() c.close(err) return err } @@ -337,7 +337,6 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro if !c.msgReader.fin { err = errors.New("previous message not read to completion") - c.readMu.unlock() c.close(fmt.Errorf("failed to get reader: %w", err)) return 0, nil, err } @@ -413,7 +412,6 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { } if err != nil { err = fmt.Errorf("failed to read: %w", err) - mr.c.readMu.unlock() mr.c.close(err) } return n, err diff --git a/write.go b/write.go index 0fbfd9cd..7b1152ce 100644 --- a/write.go +++ b/write.go @@ -109,7 +109,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) (int, error if !c.flate() { defer c.msgWriter.mu.unlock() - return c.writeFrame(true, ctx, true, false, c.msgWriter.opcode, p) + return c.writeFrame(ctx, true, false, c.msgWriter.opcode, p) } n, err := mw.Write(p) @@ -146,20 +146,19 @@ func (mw *msgWriter) putFlateWriter() { // Write writes the given bytes to the WebSocket connection. func (mw *msgWriter) Write(p []byte) (_ int, err error) { - if mw.closed { - return 0, errors.New("cannot use closed writer") - } - err = mw.writeMu.lock(mw.ctx) if err != nil { return 0, fmt.Errorf("failed to write: %w", err) } defer mw.writeMu.unlock() + if mw.closed { + return 0, errors.New("cannot use closed writer") + } + defer func() { if err != nil { err = fmt.Errorf("failed to write: %w", err) - mw.writeMu.unlock() mw.c.close(err) } }() @@ -180,7 +179,7 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { } func (mw *msgWriter) write(p []byte) (int, error) { - n, err := mw.c.writeFrame(true, mw.ctx, false, mw.flate, mw.opcode, p) + n, err := mw.c.writeFrame(mw.ctx, false, mw.flate, mw.opcode, p) if err != nil { return n, fmt.Errorf("failed to write data frame: %w", err) } @@ -210,7 +209,7 @@ func (mw *msgWriter) Close() (err error) { } } - _, err = mw.c.writeFrame(true, mw.ctx, true, mw.flate, mw.opcode, nil) + _, err = mw.c.writeFrame(mw.ctx, true, mw.flate, mw.opcode, nil) if err != nil { return fmt.Errorf("failed to write fin frame: %w", err) } @@ -236,7 +235,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error ctx, cancel := context.WithTimeout(ctx, time.Second*5) defer cancel() - _, err := c.writeFrame(false, ctx, true, false, opcode, p) + _, err := c.writeFrame(ctx, true, false, opcode, p) if err != nil { return fmt.Errorf("failed to write control frame %v: %w", opcode, err) } @@ -244,7 +243,7 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error } // frame handles all writes to the connection. -func (c *Conn) writeFrame(msgWriter bool, ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { +func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { err = c.writeFrameMu.lock(ctx) if err != nil { return 0, err @@ -284,10 +283,6 @@ func (c *Conn) writeFrame(msgWriter bool, ctx context.Context, fin bool, flate b err = ctx.Err() default: } - c.writeFrameMu.unlock() - if msgWriter { - c.msgWriter.writeMu.unlock() - } c.close(err) err = fmt.Errorf("failed to write frame: %w", err) } From 7b1a6bbaa14e56050770eee1161138ce58e5f39e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 03:15:26 -0700 Subject: [PATCH 100/152] README.md formatting fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8850f511..ec5d2704 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,7 @@ See GitHub issues for minor issues but the major future enhancements are: - [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246) - [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209) - [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) - - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster + - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) From d91a2124e071bbf025abedb1cdb608a94e81985c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 03:19:53 -0700 Subject: [PATCH 101/152] wsjs: Ensure no goroutines leak after Close Closes #330 --- close.go | 4 ++-- conn.go | 33 ++++++++++++++------------------- ws_js.go | 12 ++++++++++-- 3 files changed, 26 insertions(+), 23 deletions(-) diff --git a/close.go b/close.go index 1053751c..c3dee7e0 100644 --- a/close.go +++ b/close.go @@ -99,14 +99,14 @@ func CloseStatus(err error) StatusCode { // Close will unblock all goroutines interacting with the connection once // complete. func (c *Conn) Close(code StatusCode, reason string) error { - defer c.wgWait() + defer c.wg.Wait() return c.closeHandshake(code, reason) } // CloseNow closes the WebSocket connection without attempting a close handshake. // Use when you do not want the overhead of the close handshake. func (c *Conn) CloseNow() (err error) { - defer c.wgWait() + defer c.wg.Wait() defer errd.Wrap(&err, "failed to close WebSocket") if c.isClosed() { diff --git a/conn.go b/conn.go index 05531c3b..e133cd67 100644 --- a/conn.go +++ b/conn.go @@ -45,8 +45,6 @@ const ( type Conn struct { noCopy - wg sync.WaitGroup - subprotocol string rwc io.ReadWriteCloser client bool @@ -72,6 +70,7 @@ type Conn struct { writeHeaderBuf [8]byte writeHeader header + wg sync.WaitGroup closed chan struct{} closeMu sync.Mutex closeErr error @@ -132,7 +131,11 @@ func newConn(cfg connConfig) *Conn { c.close(errors.New("connection garbage collected")) }) - c.wgGo(c.timeoutLoop) + c.wg.Add(1) + go func() { + defer c.wg.Done() + c.timeoutLoop() + }() return c } @@ -163,10 +166,12 @@ func (c *Conn) close(err error) { // closeErr. c.rwc.Close() - c.wgGo(func() { + c.wg.Add(1) + go func() { + defer c.wg.Done() c.msgWriter.close() c.msgReader.close() - }) + }() } func (c *Conn) timeoutLoop() { @@ -183,9 +188,11 @@ func (c *Conn) timeoutLoop() { case <-readCtx.Done(): c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - c.wgGo(func() { + c.wg.Add(1) + go func() { + defer c.wg.Done() c.writeError(StatusPolicyViolation, errors.New("read timed out")) - }) + }() case <-writeCtx.Done(): c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) return @@ -302,15 +309,3 @@ func (m *mu) unlock() { type noCopy struct{} func (*noCopy) Lock() {} - -func (c *Conn) wgGo(fn func()) { - c.wg.Add(1) - go func() { - defer c.wg.Done() - fn() - }() -} - -func (c *Conn) wgWait() { - c.wg.Wait() -} diff --git a/ws_js.go b/ws_js.go index 180d0564..b4011b5c 100644 --- a/ws_js.go +++ b/ws_js.go @@ -47,6 +47,7 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit xsync.Int64 + wg sync.WaitGroup closingMu sync.Mutex isReadClosed xsync.Int64 closeOnce sync.Once @@ -223,6 +224,7 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { // or the connection is closed. // It thus performs the full WebSocket close handshake. func (c *Conn) Close(code StatusCode, reason string) error { + defer c.wg.Wait() err := c.exportedClose(code, reason) if err != nil { return fmt.Errorf("failed to close WebSocket: %w", err) @@ -236,6 +238,7 @@ func (c *Conn) Close(code StatusCode, reason string) error { // note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close // a WebSocket without the close handshake. func (c *Conn) CloseNow() error { + defer c.wg.Wait() return c.Close(StatusGoingAway, "") } @@ -388,10 +391,15 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { c.isReadClosed.Store(1) ctx, cancel := context.WithCancel(ctx) + c.wg.Add(1) go func() { + defer c.CloseNow() + defer c.wg.Done() defer cancel() - c.read(ctx) - c.Close(StatusPolicyViolation, "unexpected data message") + _, _, err := c.read(ctx) + if err != nil { + c.Close(StatusPolicyViolation, "unexpected data message") + } }() return ctx } From 0caa99775940b191e81610fbb73acb5447401da9 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 03:26:56 -0700 Subject: [PATCH 102/152] Another README.md update --- README.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index ec5d2704..3bf51e56 100644 --- a/README.md +++ b/README.md @@ -140,4 +140,15 @@ to nhooyr.io/websocket. [gobwas/ws](https://github.com/gobwas/ws) has an extremely flexible API that allows it to be used in an event driven style for performance. See the author's [blog post](https://medium.freecodecamp.org/million-websockets-and-go-cc58418460bb). -However when writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. +However it is quite bloated. See https://pkg.go.dev/github.com/gobwas/ws + +When writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. + +#### lesismal/nbio + +[lesismal/nbio](https://github.com/lesismal/nbio) is similar to gobwas/ws in that the API is +event driven for performance reasons. + +However it is quite bloated. See https://pkg.go.dev/github.com/lesismal/nbio + +When writing idiomatic Go, nhooyr.io/websocket will be faster and easier to use. From 7d8ddbc72c3a58f29e211fa2b490fa1d38b3d666 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 05:07:07 -0700 Subject: [PATCH 103/152] Fix in README.md --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index 3bf51e56..7fa3177b 100644 --- a/README.md +++ b/README.md @@ -115,7 +115,6 @@ Advantages of nhooyr.io/websocket: - Gorilla writes directly to a net.Conn and so duplicates features of net/http.Client. - Concurrent writes - Close handshake ([gorilla/websocket#448](https://github.com/gorilla/websocket/issues/448)) -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Idiomatic [ping pong](https://pkg.go.dev/nhooyr.io/websocket#Conn.Ping) API - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) @@ -125,7 +124,7 @@ Advantages of nhooyr.io/websocket: Soon we'll have assembly and be 4.5x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode -- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) +- [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) #### golang.org/x/net/websocket From 535fd2c0516e074fbd5f8340eb3e0d345975bb24 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 06:46:50 -0700 Subject: [PATCH 104/152] go.sum: Delete No longer needed :) --- go.sum | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 go.sum diff --git a/go.sum b/go.sum deleted file mode 100644 index e69de29b..00000000 From 63c0405b4e4735ab744a8b1bf5bce15e4ff99689 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 07:32:34 -0700 Subject: [PATCH 105/152] ci/fmt.sh: Tidy internal module dependencies --- ci/fmt.sh | 2 ++ internal/examples/go.mod | 5 ++--- internal/examples/go.sum | 39 -------------------------------------- internal/thirdparty/go.mod | 2 +- internal/thirdparty/go.sum | 1 - 5 files changed, 5 insertions(+), 44 deletions(-) diff --git a/ci/fmt.sh b/ci/fmt.sh index 0d902732..6e5a68e4 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -3,6 +3,8 @@ set -eu cd -- "$(dirname "$0")/.." go mod tidy +(cd ./internal/thirdparty && go mod tidy) +(cd ./internal/examples && go mod tidy) gofmt -w -s . go run golang.org/x/tools/cmd/goimports@latest -w "-local=$(go list -m)" . diff --git a/internal/examples/go.mod b/internal/examples/go.mod index b5cdcc1d..c98b81ce 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -5,7 +5,6 @@ go 1.19 replace nhooyr.io/websocket => ../.. require ( - github.com/klauspost/compress v1.10.3 // indirect - golang.org/x/time v0.3.0 // indirect - nhooyr.io/websocket v1.8.7 // indirect + golang.org/x/time v0.3.0 + nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) diff --git a/internal/examples/go.sum b/internal/examples/go.sum index 03aa32c2..f8a07e82 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,41 +1,2 @@ -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= -github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= -github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= -github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= -github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= -github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= -github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= -github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= -github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= -github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= -github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= -github.com/klauspost/compress v1.10.3 h1:OP96hzwJVBIHYU52pVTI6CczrxPvrGfgqF9N5eTO0Q8= -github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= -github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= -github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= -github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= -nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index e8c3e2c0..10eb45c1 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -8,7 +8,7 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/gobwas/ws v1.3.0 github.com/gorilla/websocket v1.5.0 - nhooyr.io/websocket v1.8.7 + nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) require ( diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 80e4ad52..a9424b8d 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -14,7 +14,6 @@ github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= -github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= From 7ada24994a18ed4a3e59ca206ea9783f422e3718 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 07:47:45 -0700 Subject: [PATCH 106/152] Fix typo in README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 7fa3177b..1c5751d8 100644 --- a/README.md +++ b/README.md @@ -121,7 +121,7 @@ Advantages of nhooyr.io/websocket: - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage - [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Soon we'll have assembly and be 4.5x faster [#326](https://github.com/nhooyr/websocket/pull/326) + Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) From ff3ea39ba06d07d4980b64c0008d7d178e0d9411 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 14:39:43 -0700 Subject: [PATCH 107/152] ci/lint.sh: Remove golint Underscores in symbols are ok sometimes... --- ci/lint.sh | 4 ---- 1 file changed, 4 deletions(-) diff --git a/ci/lint.sh b/ci/lint.sh index 80f309be..3cf8eee4 100755 --- a/ci/lint.sh +++ b/ci/lint.sh @@ -5,10 +5,6 @@ cd -- "$(dirname "$0")/.." go vet ./... GOOS=js GOARCH=wasm go vet ./... -go install golang.org/x/lint/golint@latest -golint -set_exit_status ./... -GOOS=js GOARCH=wasm golint -set_exit_status ./... - go install honnef.co/go/tools/cmd/staticcheck@latest staticcheck ./... GOOS=js GOARCH=wasm staticcheck ./... From af0fd9d45e6e56b045f8e8556aa8fe917cbc6259 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 15:19:48 -0700 Subject: [PATCH 108/152] examples/chat: Fix race condition Tricky tricky. --- internal/examples/chat/chat.go | 39 +++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 12 deletions(-) diff --git a/internal/examples/chat/chat.go b/internal/examples/chat/chat.go index 78a5696a..8b1e30c1 100644 --- a/internal/examples/chat/chat.go +++ b/internal/examples/chat/chat.go @@ -5,6 +5,7 @@ import ( "errors" "io" "log" + "net" "net/http" "sync" "time" @@ -69,14 +70,7 @@ func (cs *chatServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { // subscribeHandler accepts the WebSocket connection and then subscribes // it to all future messages. func (cs *chatServer) subscribeHandler(w http.ResponseWriter, r *http.Request) { - c, err := websocket.Accept(w, r, nil) - if err != nil { - cs.logf("%v", err) - return - } - defer c.CloseNow() - - err = cs.subscribe(r.Context(), c) + err := cs.subscribe(r.Context(), w, r) if errors.Is(err, context.Canceled) { return } @@ -117,18 +111,39 @@ func (cs *chatServer) publishHandler(w http.ResponseWriter, r *http.Request) { // // It uses CloseRead to keep reading from the connection to process control // messages and cancel the context if the connection drops. -func (cs *chatServer) subscribe(ctx context.Context, c *websocket.Conn) error { - ctx = c.CloseRead(ctx) - +func (cs *chatServer) subscribe(ctx context.Context, w http.ResponseWriter, r *http.Request) error { + var mu sync.Mutex + var c *websocket.Conn + var closed bool s := &subscriber{ msgs: make(chan []byte, cs.subscriberMessageBuffer), closeSlow: func() { - c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") + mu.Lock() + defer mu.Unlock() + closed = true + if c != nil { + c.Close(websocket.StatusPolicyViolation, "connection too slow to keep up with messages") + } }, } cs.addSubscriber(s) defer cs.deleteSubscriber(s) + c2, err := websocket.Accept(w, r, nil) + if err != nil { + return err + } + mu.Lock() + if closed { + mu.Unlock() + return net.ErrClosed + } + c = c2 + mu.Unlock() + defer c.CloseNow() + + ctx = c.CloseRead(ctx) + for { select { case msg := <-s.msgs: From 84d7ddcc862f618e5eb1cb0beb9599a30001eb4d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 09:04:40 -0700 Subject: [PATCH 109/152] README.md: Update badges --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 1c5751d8..85d2eb31 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,7 @@ # websocket -[![godoc](https://godoc.org/nhooyr.io/websocket?status.svg)](https://pkg.go.dev/nhooyr.io/websocket) -[![coverage](https://img.shields.io/badge/coverage-91%25-success)](https://nhooyr.io/websocket/coverage.html) +[![Go Reference](https://pkg.go.dev/badge/nhooyr.io/websocket.svg)](https://pkg.go.dev/nhooyr.io/websocket) +[![Go Coverage](https://img.shields.io/badge/coverage-91%25-success)](https://nhooyr.io/websocket/coverage.html) websocket is a minimal and idiomatic WebSocket library for Go. From b4e4f4f12ee47d9827cee352c52b612944bad6bf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 09:13:59 -0700 Subject: [PATCH 110/152] Don't embed noCopy... Whoops. --- conn.go | 2 +- ws_js.go | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/conn.go b/conn.go index e133cd67..ef4d62ad 100644 --- a/conn.go +++ b/conn.go @@ -43,7 +43,7 @@ const ( // This applies to context expirations as well unfortunately. // See https://github.com/nhooyr/websocket/issues/242#issuecomment-633182220 type Conn struct { - noCopy + noCopy noCopy subprotocol string rwc io.ReadWriteCloser diff --git a/ws_js.go b/ws_js.go index b4011b5c..cf119da7 100644 --- a/ws_js.go +++ b/ws_js.go @@ -41,7 +41,7 @@ const ( // Conn provides a wrapper around the browser WebSocket API. type Conn struct { - noCopy + noCopy noCopy ws wsjs.WebSocket // read limit for a message in bytes. From 454aee86997aeb75f06c6cecbc15c3355b5e8d30 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 25 Oct 2023 05:39:27 -0700 Subject: [PATCH 111/152] ws_js.go: Disable read limit on -1 Whoops, updates #254 and closes #410 --- ws_js.go | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/ws_js.go b/ws_js.go index cf119da7..77d0d80f 100644 --- a/ws_js.go +++ b/ws_js.go @@ -42,7 +42,7 @@ const ( // Conn provides a wrapper around the browser WebSocket API. type Conn struct { noCopy noCopy - ws wsjs.WebSocket + ws wsjs.WebSocket // read limit for a message in bytes. msgReadLimit xsync.Int64 @@ -138,7 +138,8 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { if err != nil { return 0, nil, fmt.Errorf("failed to read: %w", err) } - if int64(len(p)) > c.msgReadLimit.Load() { + readLimit := c.msgReadLimit.Load() + if readLimit >= 0 && int64(len(p)) > readLimit { err := fmt.Errorf("read limited at %v bytes", c.msgReadLimit.Load()) c.Close(StatusMessageTooBig, err.Error()) return 0, nil, err From 8060f3a3b51679f8d2f48e04113ad596612bca50 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 25 Oct 2023 06:05:22 -0700 Subject: [PATCH 112/152] README.md: Mention gorilla advantage re no extra context cancellation goroutine Not sure how/when this was lost but an important disadvantage to note. --- README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/README.md b/README.md index 85d2eb31..d093746d 100644 --- a/README.md +++ b/README.md @@ -102,6 +102,8 @@ Advantages of [gorilla/websocket](https://github.com/gorilla/websocket): - Mature and widely used - [Prepared writes](https://pkg.go.dev/github.com/gorilla/websocket#PreparedMessage) - Configurable [buffer sizes](https://pkg.go.dev/github.com/gorilla/websocket#hdr-Buffers) +- No extra goroutine per connection to support cancellation with context.Context. This costs nhooyr.io/websocket 2 KB of memory per connection. + - Will be removed soon with [context.AfterFunc](https://github.com/golang/go/issues/57928). See [#411](https://github.com/nhooyr/websocket/issues/411) Advantages of nhooyr.io/websocket: From 52721a9fc36a5c1cadecc124dd0a08184e929681 Mon Sep 17 00:00:00 2001 From: Kunal Singh Date: Thu, 26 Oct 2023 00:03:11 +0530 Subject: [PATCH 113/152] Use ws:// over http:// in example logs --- internal/examples/chat/README.md | 2 +- internal/examples/chat/main.go | 2 +- internal/examples/echo/README.md | 2 +- internal/examples/echo/main.go | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/internal/examples/chat/README.md b/internal/examples/chat/README.md index ca1024a0..574c6994 100644 --- a/internal/examples/chat/README.md +++ b/internal/examples/chat/README.md @@ -5,7 +5,7 @@ This directory contains a full stack example of a simple chat webapp using nhooy ```bash $ cd examples/chat $ go run . localhost:0 -listening on http://127.0.0.1:51055 +listening on ws://127.0.0.1:51055 ``` Visit the printed URL to submit and view broadcasted messages in a browser. diff --git a/internal/examples/chat/main.go b/internal/examples/chat/main.go index 3fcec6be..e3432984 100644 --- a/internal/examples/chat/main.go +++ b/internal/examples/chat/main.go @@ -31,7 +31,7 @@ func run() error { if err != nil { return err } - log.Printf("listening on http://%v", l.Addr()) + log.Printf("listening on ws://%v", l.Addr()) cs := newChatServer() s := &http.Server{ diff --git a/internal/examples/echo/README.md b/internal/examples/echo/README.md index 7f42c3c5..ac03f640 100644 --- a/internal/examples/echo/README.md +++ b/internal/examples/echo/README.md @@ -5,7 +5,7 @@ This directory contains a echo server example using nhooyr.io/websocket. ```bash $ cd examples/echo $ go run . localhost:0 -listening on http://127.0.0.1:51055 +listening on ws://127.0.0.1:51055 ``` You can use a WebSocket client like https://github.com/hashrocket/ws to connect. All messages diff --git a/internal/examples/echo/main.go b/internal/examples/echo/main.go index 16d78a79..47e30d05 100644 --- a/internal/examples/echo/main.go +++ b/internal/examples/echo/main.go @@ -31,7 +31,7 @@ func run() error { if err != nil { return err } - log.Printf("listening on http://%v", l.Addr()) + log.Printf("listening on ws://%v", l.Addr()) s := &http.Server{ Handler: echoServer{ From 5df0303d0a24d67de232a55f55ff3cbf9f8fc6ac Mon Sep 17 00:00:00 2001 From: wdvxdr Date: Mon, 24 Jan 2022 19:25:11 +0800 Subject: [PATCH 114/152] mask.go: Use SIMD masking for amd64 and arm64 goos: windows goarch: amd64 pkg: nhooyr.io/websocket cpu: Intel(R) Core(TM) i5-9300H CPU @ 2.40GHz Benchmark_mask/2/basic-8 425339004 2.795 ns/op 715.66 MB/s Benchmark_mask/2/nhooyr-8 379937766 3.186 ns/op 627.78 MB/s Benchmark_mask/2/gorilla-8 392164167 3.071 ns/op 651.24 MB/s Benchmark_mask/2/gobwas-8 310037222 3.880 ns/op 515.46 MB/s Benchmark_mask/3/basic-8 321408024 3.806 ns/op 788.32 MB/s Benchmark_mask/3/nhooyr-8 350726338 3.478 ns/op 862.58 MB/s Benchmark_mask/3/gorilla-8 332217727 3.634 ns/op 825.43 MB/s Benchmark_mask/3/gobwas-8 247376214 4.886 ns/op 614.01 MB/s Benchmark_mask/4/basic-8 261182472 4.582 ns/op 872.91 MB/s Benchmark_mask/4/nhooyr-8 381830712 3.262 ns/op 1226.05 MB/s Benchmark_mask/4/gorilla-8 272616304 4.395 ns/op 910.04 MB/s Benchmark_mask/4/gobwas-8 204574558 5.855 ns/op 683.19 MB/s Benchmark_mask/8/basic-8 191330037 6.162 ns/op 1298.24 MB/s Benchmark_mask/8/nhooyr-8 369694992 3.285 ns/op 2435.65 MB/s Benchmark_mask/8/gorilla-8 175388466 6.743 ns/op 1186.48 MB/s Benchmark_mask/8/gobwas-8 241719933 4.886 ns/op 1637.45 MB/s Benchmark_mask/16/basic-8 100000000 10.92 ns/op 1464.83 MB/s Benchmark_mask/16/nhooyr-8 272565096 4.436 ns/op 3606.98 MB/s Benchmark_mask/16/gorilla-8 100000000 11.20 ns/op 1428.53 MB/s Benchmark_mask/16/gobwas-8 221356798 5.405 ns/op 2960.45 MB/s Benchmark_mask/32/basic-8 61476984 20.40 ns/op 1568.80 MB/s Benchmark_mask/32/nhooyr-8 238665572 5.050 ns/op 6337.22 MB/s Benchmark_mask/32/gorilla-8 100000000 12.09 ns/op 2647.28 MB/s Benchmark_mask/32/gobwas-8 186077235 6.477 ns/op 4940.36 MB/s Benchmark_mask/128/basic-8 14629720 80.90 ns/op 1582.19 MB/s Benchmark_mask/128/nhooyr-8 181241968 6.565 ns/op 19497.98 MB/s Benchmark_mask/128/gorilla-8 68308342 16.76 ns/op 7639.37 MB/s Benchmark_mask/128/gobwas-8 94582026 12.97 ns/op 9872.11 MB/s Benchmark_mask/512/basic-8 3921001 305.6 ns/op 1675.55 MB/s Benchmark_mask/512/nhooyr-8 123102199 9.721 ns/op 52669.11 MB/s Benchmark_mask/512/gorilla-8 32355914 38.18 ns/op 13411.43 MB/s Benchmark_mask/512/gobwas-8 31528501 37.80 ns/op 13544.37 MB/s Benchmark_mask/4096/basic-8 491804 2381 ns/op 1720.39 MB/s Benchmark_mask/4096/nhooyr-8 26159691 46.98 ns/op 87187.73 MB/s Benchmark_mask/4096/gorilla-8 4898440 243.6 ns/op 16817.89 MB/s Benchmark_mask/4096/gobwas-8 4336398 277.2 ns/op 14776.40 MB/s Benchmark_mask/16384/basic-8 113842 9623 ns/op 1702.66 MB/s Benchmark_mask/16384/nhooyr-8 8088847 154.5 ns/op 106058.18 MB/s Benchmark_mask/16384/gorilla-8 1282993 933.6 ns/op 17549.90 MB/s Benchmark_mask/16384/gobwas-8 997347 1086 ns/op 15093.49 MB/s We're about 4-5x faster then gorilla now. --- frame.go | 2 +- go.mod | 2 + go.sum | 2 + mask_amd64.s | 152 ++++++++++++++++++++++++++++++++++++++++++++++++ mask_arm64.s | 74 +++++++++++++++++++++++ mask_asm.go | 19 ++++++ mask_generic.go | 7 +++ 7 files changed, 257 insertions(+), 1 deletion(-) create mode 100644 go.sum create mode 100644 mask_amd64.s create mode 100644 mask_arm64.s create mode 100644 mask_asm.go create mode 100644 mask_generic.go diff --git a/frame.go b/frame.go index 351632fd..eec15bb9 100644 --- a/frame.go +++ b/frame.go @@ -184,7 +184,7 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { // to be in little endian. // // See https://github.com/golang/go/issues/31586 -func mask(key uint32, b []byte) uint32 { +func maskGo(key uint32, b []byte) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/go.mod b/go.mod index 715a9f7a..285b955f 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module nhooyr.io/websocket go 1.19 + +require golang.org/x/sys v0.13.0 diff --git a/go.sum b/go.sum new file mode 100644 index 00000000..d4673ecf --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/mask_amd64.s b/mask_amd64.s new file mode 100644 index 00000000..caca53ec --- /dev/null +++ b/mask_amd64.s @@ -0,0 +1,152 @@ +#include "textflag.h" + +// func maskAsm(b *byte, len int, key uint32) +TEXT ·maskAsm(SB), NOSPLIT, $0-28 + // AX = b + // CX = len (left length) + // SI = key (uint32) + // DI = uint64(SI) | uint64(SI)<<32 + MOVQ b+0(FP), AX + MOVQ len+8(FP), CX + MOVL key+16(FP), SI + + // calculate the DI + // DI = SI<<32 | SI + MOVL SI, DI + MOVQ DI, DX + SHLQ $32, DI + ORQ DX, DI + + CMPQ CX, $15 + JLE less_than_16 + CMPQ CX, $63 + JLE less_than_64 + CMPQ CX, $128 + JLE sse + TESTQ $31, AX + JNZ unaligned + +aligned: + CMPB ·useAVX2(SB), $1 + JE avx2 + JMP sse + +unaligned_loop_1byte: + XORB SI, (AX) + INCQ AX + DECQ CX + ROLL $24, SI + TESTQ $7, AX + JNZ unaligned_loop_1byte + + // calculate DI again since SI was modified + // DI = SI<<32 | SI + MOVL SI, DI + MOVQ DI, DX + SHLQ $32, DI + ORQ DX, DI + + TESTQ $31, AX + JZ aligned + +unaligned: + TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. + JNZ unaligned_loop_1byte + +unaligned_loop: + // we don't need to check the CX since we know it's above 128 + XORQ DI, (AX) + ADDQ $8, AX + SUBQ $8, CX + TESTQ $31, AX + JNZ unaligned_loop + JMP aligned + +avx2: + CMPQ CX, $0x80 + JL sse + VMOVQ DI, X0 + VPBROADCASTQ X0, Y0 + +avx2_loop: + VPXOR (AX), Y0, Y1 + VPXOR 32(AX), Y0, Y2 + VPXOR 64(AX), Y0, Y3 + VPXOR 96(AX), Y0, Y4 + VMOVDQU Y1, (AX) + VMOVDQU Y2, 32(AX) + VMOVDQU Y3, 64(AX) + VMOVDQU Y4, 96(AX) + ADDQ $0x80, AX + SUBQ $0x80, CX + CMPQ CX, $0x80 + JAE avx2_loop // loop if CX >= 0x80 + +sse: + CMPQ CX, $0x40 + JL less_than_64 + MOVQ DI, X0 + PUNPCKLQDQ X0, X0 + +sse_loop: + MOVOU 0*16(AX), X1 + MOVOU 1*16(AX), X2 + MOVOU 2*16(AX), X3 + MOVOU 3*16(AX), X4 + PXOR X0, X1 + PXOR X0, X2 + PXOR X0, X3 + PXOR X0, X4 + MOVOU X1, 0*16(AX) + MOVOU X2, 1*16(AX) + MOVOU X3, 2*16(AX) + MOVOU X4, 3*16(AX) + ADDQ $0x40, AX + SUBQ $0x40, CX + CMPQ CX, $0x40 + JAE sse_loop + +less_than_64: + TESTQ $32, CX + JZ less_than_32 + XORQ DI, (AX) + XORQ DI, 8(AX) + XORQ DI, 16(AX) + XORQ DI, 24(AX) + ADDQ $32, AX + +less_than_32: + TESTQ $16, CX + JZ less_than_16 + XORQ DI, (AX) + XORQ DI, 8(AX) + ADDQ $16, AX + +less_than_16: + TESTQ $8, CX + JZ less_than_8 + XORQ DI, (AX) + ADDQ $8, AX + +less_than_8: + TESTQ $4, CX + JZ less_than_4 + XORL SI, (AX) + ADDQ $4, AX + +less_than_4: + TESTQ $2, CX + JZ less_than_2 + XORW SI, (AX) + ROLL $16, SI + ADDQ $2, AX + +less_than_2: + TESTQ $1, CX + JZ done + XORB SI, (AX) + ROLL $24, SI + +done: + MOVL SI, ret+24(FP) + RET diff --git a/mask_arm64.s b/mask_arm64.s new file mode 100644 index 00000000..624cb720 --- /dev/null +++ b/mask_arm64.s @@ -0,0 +1,74 @@ +#include "textflag.h" + +// func maskAsm(b *byte,len, int, key uint32) +TEXT ·maskAsm(SB), NOSPLIT, $0-28 + // R0 = b + // R1 = len + // R2 = uint64(key)<<32 | uint64(key) + // R3 = key (uint32) + MOVD b_ptr+0(FP), R0 + MOVD b_len+8(FP), R1 + MOVWU key+16(FP), R3 + MOVD R3, R2 + ORR R2<<32, R2, R2 + VDUP R2, V0.D2 + CMP $64, R1 + BLT less_than_64 + + // todo: optimize unaligned case +loop_64: + VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] + VEOR V1.B16, V0.B16, V1.B16 + VEOR V2.B16, V0.B16, V2.B16 + VEOR V3.B16, V0.B16, V3.B16 + VEOR V4.B16, V0.B16, V4.B16 + VST1.P [V1.B16, V2.B16, V3.B16, V4.B16], 64(R0) + SUBS $64, R1 + CMP $64, R1 + BGE loop_64 + +less_than_64: + // quick end + CBZ R1, end + TBZ $5, R1, less_than32 + VLD1 (R0), [V1.B16, V2.B16] + VEOR V1.B16, V0.B16, V1.B16 + VEOR V2.B16, V0.B16, V2.B16 + VST1.P [V1.B16, V2.B16], 32(R0) + +less_than32: + TBZ $4, R1, less_than16 + LDP (R0), (R11, R12) + EOR R11, R2, R11 + EOR R12, R2, R12 + STP.P (R11, R12), 16(R0) + +less_than16: + TBZ $3, R1, less_than8 + MOVD (R0), R11 + EOR R2, R11, R11 + MOVD.P R11, 8(R0) + +less_than8: + TBZ $2, R1, less_than4 + MOVWU (R0), R11 + EORW R2, R11, R11 + MOVWU.P R11, 4(R0) + +less_than4: + TBZ $1, R1, less_than2 + MOVHU (R0), R11 + EORW R3, R11, R11 + MOVHU.P R11, 2(R0) + RORW $16, R3 + +less_than2: + TBZ $0, R1, end + MOVBU (R0), R11 + EORW R3, R11, R11 + MOVBU.P R11, 1(R0) + RORW $8, R3 + +end: + MOVWU R3, ret+24(FP) + RET diff --git a/mask_asm.go b/mask_asm.go new file mode 100644 index 00000000..a18a20e5 --- /dev/null +++ b/mask_asm.go @@ -0,0 +1,19 @@ +//go:build !appengine && (amd64 || arm64) +// +build !appengine +// +build amd64 arm64 + +package websocket + +import "golang.org/x/sys/cpu" + +func mask(key uint32, b []byte) uint32 { + if len(b) > 0 { + return maskAsm(&b[0], len(b), key) + } + return key +} + +var useAVX2 = cpu.X86.HasAVX2 + +//go:noescape +func maskAsm(b *byte, len int, key uint32) uint32 diff --git a/mask_generic.go b/mask_generic.go new file mode 100644 index 00000000..6331b746 --- /dev/null +++ b/mask_generic.go @@ -0,0 +1,7 @@ +//go:build appengine || (!amd64 && !arm64 && !js) + +package websocket + +func mask(key uint32, b []byte) uint32 { + return maskGo(key, b) +} From cda2170e9b48e7a33e4b5401eb6db31cd61314d4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 07:41:22 -0700 Subject: [PATCH 115/152] Refactor and compile masking code again --- frame.go | 2 +- internal/examples/go.mod | 2 ++ internal/examples/go.sum | 2 ++ internal/thirdparty/go.mod | 2 +- internal/thirdparty/go.sum | 4 ++-- mask_generic.go => mask.go | 2 +- mask_asm.go | 6 ++---- 7 files changed, 11 insertions(+), 9 deletions(-) rename mask_generic.go => mask.go (63%) diff --git a/frame.go b/frame.go index eec15bb9..362a99e9 100644 --- a/frame.go +++ b/frame.go @@ -173,7 +173,7 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { return nil } -// mask applies the WebSocket masking algorithm to p +// maskGo applies the WebSocket masking algorithm to p // with the given key. // See https://tools.ietf.org/html/rfc6455#section-5.3 // diff --git a/internal/examples/go.mod b/internal/examples/go.mod index c98b81ce..dfdb8cca 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -8,3 +8,5 @@ require ( golang.org/x/time v0.3.0 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) + +require golang.org/x/sys v0.13.0 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index f8a07e82..1931a8f2 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,2 +1,4 @@ +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index 10eb45c1..3f32a416 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -34,7 +34,7 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.8.0 // indirect + golang.org/x/sys v0.13.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index a9424b8d..47f324bb 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -76,8 +76,8 @@ golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0 h1:EBmGv8NaZBZTWvrbjNoL6HVt+IVy3QDQpJs7VRIw3tU= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= +golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= diff --git a/mask_generic.go b/mask.go similarity index 63% rename from mask_generic.go rename to mask.go index 6331b746..7c9fe308 100644 --- a/mask_generic.go +++ b/mask.go @@ -1,4 +1,4 @@ -//go:build appengine || (!amd64 && !arm64 && !js) +//go:build !amd64 && !arm64 && !js package websocket diff --git a/mask_asm.go b/mask_asm.go index a18a20e5..9b370690 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -1,6 +1,4 @@ -//go:build !appengine && (amd64 || arm64) -// +build !appengine -// +build amd64 arm64 +//go:build amd64 || arm64 package websocket @@ -13,7 +11,7 @@ func mask(key uint32, b []byte) uint32 { return key } -var useAVX2 = cpu.X86.HasAVX2 +var useAVX2 = cpu.X86.HasAVX2 //lint:ignore U1000 mask_amd64.s //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From f5397ae3d1bfdf120ce8a598a0b0b0fe2b86f784 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 07:48:47 -0700 Subject: [PATCH 116/152] mask_asm.go: Disable AVX2 Slower for some reason than just SIMD. Also no dependency on cpu package is nice. --- go.mod | 2 -- go.sum | 2 -- internal/examples/go.mod | 2 -- internal/examples/go.sum | 2 -- mask_asm.go | 4 +--- 5 files changed, 1 insertion(+), 11 deletions(-) diff --git a/go.mod b/go.mod index 285b955f..715a9f7a 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module nhooyr.io/websocket go 1.19 - -require golang.org/x/sys v0.13.0 diff --git a/go.sum b/go.sum index d4673ecf..e69de29b 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= diff --git a/internal/examples/go.mod b/internal/examples/go.mod index dfdb8cca..c98b81ce 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -8,5 +8,3 @@ require ( golang.org/x/time v0.3.0 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) - -require golang.org/x/sys v0.13.0 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index 1931a8f2..f8a07e82 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,4 +1,2 @@ -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/mask_asm.go b/mask_asm.go index 9b370690..946bc0f6 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,8 +2,6 @@ package websocket -import "golang.org/x/sys/cpu" - func mask(key uint32, b []byte) uint32 { if len(b) > 0 { return maskAsm(&b[0], len(b), key) @@ -11,7 +9,7 @@ func mask(key uint32, b []byte) uint32 { return key } -var useAVX2 = cpu.X86.HasAVX2 //lint:ignore U1000 mask_amd64.s +var useAVX2 = false //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From 14172e5b461e3b8376d79d2456800b7e100a044b Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 08:05:25 -0700 Subject: [PATCH 117/152] Benchmark pure go masking algorithm separately from assembly --- internal/thirdparty/frame_test.go | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go index 1a0ed125..dd0440db 100644 --- a/internal/thirdparty/frame_test.go +++ b/internal/thirdparty/frame_test.go @@ -26,6 +26,9 @@ func gorillaMaskBytes(key [4]byte, pos int, b []byte) int //go:linkname mask nhooyr.io/websocket.mask func mask(key32 uint32, b []byte) int +//go:linkname maskGo nhooyr.io/websocket.maskGo +func maskGo(key32 uint32, b []byte) int + func Benchmark_mask(b *testing.B) { sizes := []int{ 2, @@ -54,7 +57,18 @@ func Benchmark_mask(b *testing.B) { }, { - name: "nhooyr", + name: "nhooyr-go", + fn: func(b *testing.B, key [4]byte, p []byte) { + key32 := binary.LittleEndian.Uint32(key[:]) + b.ResetTimer() + + for i := 0; i < b.N; i++ { + maskGo(key32, p) + } + }, + }, + { + name: "wdvxdr1123-asm", fn: func(b *testing.B, key [4]byte, p []byte) { key32 := binary.LittleEndian.Uint32(key[:]) b.ResetTimer() @@ -64,6 +78,7 @@ func Benchmark_mask(b *testing.B) { } }, }, + { name: "gorilla", fn: func(b *testing.B, key [4]byte, p []byte) { From 685a56e22e4ffe94d73c1da0d0e8746dcd36f165 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 08:08:37 -0700 Subject: [PATCH 118/152] Update README.md to indicate assembly websocket masking --- README.md | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index d093746d..4b2d828e 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,7 @@ go get nhooyr.io/websocket - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) +- WebSocket masking implemented in assembly for amd64 and arm64 [#326](https://github.com/nhooyr/websocket/issues/326) ## Roadmap @@ -36,8 +37,6 @@ See GitHub issues for minor issues but the major future enhancements are: - [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267) - [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246) - [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209) -- [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) - - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) @@ -121,7 +120,7 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go +- [3x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support From cb7509ab70e9f9ca4ce47a2eb90ff86ebaee2d28 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 08:47:44 -0700 Subject: [PATCH 119/152] mask_amd64.s: Remove AVX2 fully --- mask_amd64.s | 29 ++--------------------------- mask_arm64.s | 1 - mask_asm.go | 2 -- 3 files changed, 2 insertions(+), 30 deletions(-) diff --git a/mask_amd64.s b/mask_amd64.s index caca53ec..bd42be31 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -26,11 +26,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 TESTQ $31, AX JNZ unaligned -aligned: - CMPB ·useAVX2(SB), $1 - JE avx2 - JMP sse - unaligned_loop_1byte: XORB SI, (AX) INCQ AX @@ -47,7 +42,7 @@ unaligned_loop_1byte: ORQ DX, DI TESTQ $31, AX - JZ aligned + JZ sse unaligned: TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. @@ -60,27 +55,7 @@ unaligned_loop: SUBQ $8, CX TESTQ $31, AX JNZ unaligned_loop - JMP aligned - -avx2: - CMPQ CX, $0x80 - JL sse - VMOVQ DI, X0 - VPBROADCASTQ X0, Y0 - -avx2_loop: - VPXOR (AX), Y0, Y1 - VPXOR 32(AX), Y0, Y2 - VPXOR 64(AX), Y0, Y3 - VPXOR 96(AX), Y0, Y4 - VMOVDQU Y1, (AX) - VMOVDQU Y2, 32(AX) - VMOVDQU Y3, 64(AX) - VMOVDQU Y4, 96(AX) - ADDQ $0x80, AX - SUBQ $0x80, CX - CMPQ CX, $0x80 - JAE avx2_loop // loop if CX >= 0x80 + JMP sse sse: CMPQ CX, $0x40 diff --git a/mask_arm64.s b/mask_arm64.s index 624cb720..b3d48e68 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -15,7 +15,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMP $64, R1 BLT less_than_64 - // todo: optimize unaligned case loop_64: VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] VEOR V1.B16, V0.B16, V1.B16 diff --git a/mask_asm.go b/mask_asm.go index 946bc0f6..34021fa7 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -9,7 +9,5 @@ func mask(key uint32, b []byte) uint32 { return key } -var useAVX2 = false - //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From 3f8c9e07bcaa0a223d092b618c34ca7dba3521db Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 09:20:31 -0700 Subject: [PATCH 120/152] mask_amd64.s: Minor improvements --- frame.go | 2 ++ mask_amd64.s | 12 ++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/frame.go b/frame.go index 362a99e9..ff09ec26 100644 --- a/frame.go +++ b/frame.go @@ -184,6 +184,8 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { // to be in little endian. // // See https://github.com/golang/go/issues/31586 +// +//lint:ignore U1000 mask.go func maskGo(key uint32, b []byte) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/mask_amd64.s b/mask_amd64.s index bd42be31..935232fa 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -17,8 +17,8 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 SHLQ $32, DI ORQ DX, DI - CMPQ CX, $15 - JLE less_than_16 + CMPQ CX, $7 + JLE less_than_8 CMPQ CX, $63 JLE less_than_64 CMPQ CX, $128 @@ -58,7 +58,7 @@ unaligned_loop: JMP sse sse: - CMPQ CX, $0x40 + CMPQ CX, $64 JL less_than_64 MOVQ DI, X0 PUNPCKLQDQ X0, X0 @@ -76,9 +76,9 @@ sse_loop: MOVOU X2, 1*16(AX) MOVOU X3, 2*16(AX) MOVOU X4, 3*16(AX) - ADDQ $0x40, AX - SUBQ $0x40, CX - CMPQ CX, $0x40 + ADDQ $64, AX + SUBQ $64, CX + CMPQ CX, $64 JAE sse_loop less_than_64: From 367743dc6fe48866a91ef88296699449cca17d4d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 15:57:47 -0700 Subject: [PATCH 121/152] mask_amd64.sh: Cleanup --- mask_amd64.s | 23 ++++++++++++----------- mask_arm64.s | 4 +++- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/mask_amd64.s b/mask_amd64.s index 935232fa..905d7e4a 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -10,18 +10,18 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 MOVQ len+8(FP), CX MOVL key+16(FP), SI - // calculate the DI - // DI = SI<<32 | SI + // Calculate the DI aka the uint64 key. + // DI = uint64(SI) | uint64(SI)<<32 MOVL SI, DI MOVQ DI, DX SHLQ $32, DI ORQ DX, DI - CMPQ CX, $7 - JLE less_than_8 - CMPQ CX, $63 - JLE less_than_64 - CMPQ CX, $128 + CMPQ CX, $8 + JL less_than_8 + CMPQ CX, $64 + JL less_than_64 + CMPQ CX, $512 JLE sse TESTQ $31, AX JNZ unaligned @@ -34,8 +34,8 @@ unaligned_loop_1byte: TESTQ $7, AX JNZ unaligned_loop_1byte - // calculate DI again since SI was modified - // DI = SI<<32 | SI + // Calculate DI again since SI was modified. + // DI = uint64(SI) | uint64(SI)<<32 MOVL SI, DI MOVQ DI, DX SHLQ $32, DI @@ -45,11 +45,12 @@ unaligned_loop_1byte: JZ sse unaligned: - TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. + // $7 & len, if not zero jump to loop_1b. + TESTQ $7, AX JNZ unaligned_loop_1byte unaligned_loop: - // we don't need to check the CX since we know it's above 128 + // We don't need to check the CX since we know it's above 512. XORQ DI, (AX) ADDQ $8, AX SUBQ $8, CX diff --git a/mask_arm64.s b/mask_arm64.s index b3d48e68..741b77a5 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -1,6 +1,6 @@ #include "textflag.h" -// func maskAsm(b *byte,len, int, key uint32) +// func maskAsm(b *byte, len int, key uint32) TEXT ·maskAsm(SB), NOSPLIT, $0-28 // R0 = b // R1 = len @@ -15,6 +15,8 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMP $64, R1 BLT less_than_64 +// TODO: allign memory like amd64 + loop_64: VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] VEOR V1.B16, V0.B16, V1.B16 From 27f80cb8b4515ffa660eaa962aa01cd370e4c48e Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 16:24:32 -0700 Subject: [PATCH 122/152] mask.go: Cleanup assembly and add nbio benchmark --- frame.go | 2 +- frame_test.go | 2 +- internal/thirdparty/frame_test.go | 50 ++++++++++++++++++++----------- internal/thirdparty/go.mod | 2 ++ internal/thirdparty/go.sum | 36 ++++++++++++++++++++++ mask.go | 4 +-- mask_amd64.s | 2 -- mask_arm64.s | 2 +- mask_asm.go | 2 +- read.go | 4 +-- write.go | 2 +- 11 files changed, 79 insertions(+), 29 deletions(-) diff --git a/frame.go b/frame.go index ff09ec26..5d826ea3 100644 --- a/frame.go +++ b/frame.go @@ -186,7 +186,7 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { // See https://github.com/golang/go/issues/31586 // //lint:ignore U1000 mask.go -func maskGo(key uint32, b []byte) uint32 { +func maskGo(b []byte, key uint32) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/frame_test.go b/frame_test.go index e697e198..bd626358 100644 --- a/frame_test.go +++ b/frame_test.go @@ -97,7 +97,7 @@ func Test_mask(t *testing.T) { key := []byte{0xa, 0xb, 0xc, 0xff} key32 := binary.LittleEndian.Uint32(key) p := []byte{0xa, 0xb, 0xc, 0xf2, 0xc} - gotKey32 := mask(key32, p) + gotKey32 := mask(p, key32) expP := []byte{0, 0, 0, 0x0d, 0x6} assert.Equal(t, "p", expP, p) diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go index dd0440db..7202322d 100644 --- a/internal/thirdparty/frame_test.go +++ b/internal/thirdparty/frame_test.go @@ -8,11 +8,12 @@ import ( "github.com/gobwas/ws" _ "github.com/gorilla/websocket" + _ "github.com/lesismal/nbio/nbhttp/websocket" _ "nhooyr.io/websocket" ) -func basicMask(maskKey [4]byte, pos int, b []byte) int { +func basicMask(b []byte, maskKey [4]byte, pos int) int { for i := range b { b[i] ^= maskKey[pos&3] pos++ @@ -20,26 +21,30 @@ func basicMask(maskKey [4]byte, pos int, b []byte) int { return pos & 3 } -//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes -func gorillaMaskBytes(key [4]byte, pos int, b []byte) int +//go:linkname maskGo nhooyr.io/websocket.maskGo +func maskGo(b []byte, key32 uint32) int -//go:linkname mask nhooyr.io/websocket.mask -func mask(key32 uint32, b []byte) int +//go:linkname maskAsm nhooyr.io/websocket.maskAsm +func maskAsm(b *byte, len int, key32 uint32) uint32 -//go:linkname maskGo nhooyr.io/websocket.maskGo -func maskGo(key32 uint32, b []byte) int +//go:linkname nbioMaskBytes github.com/lesismal/nbio/nbhttp/websocket.maskXOR +func nbioMaskBytes(b, key []byte) int + +//go:linkname gorillaMaskBytes github.com/gorilla/websocket.maskBytes +func gorillaMaskBytes(key [4]byte, pos int, b []byte) int func Benchmark_mask(b *testing.B) { sizes := []int{ - 2, - 3, - 4, 8, 16, 32, 128, + 256, 512, + 1024, + 2048, 4096, + 8192, 16384, } @@ -51,7 +56,7 @@ func Benchmark_mask(b *testing.B) { name: "basic", fn: func(b *testing.B, key [4]byte, p []byte) { for i := 0; i < b.N; i++ { - basicMask(key, 0, p) + basicMask(p, key, 0) } }, }, @@ -63,7 +68,7 @@ func Benchmark_mask(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - maskGo(key32, p) + maskGo(p, key32) } }, }, @@ -74,7 +79,7 @@ func Benchmark_mask(b *testing.B) { b.ResetTimer() for i := 0; i < b.N; i++ { - mask(key32, p) + maskAsm(&p[0], len(p), key32) } }, }, @@ -95,16 +100,25 @@ func Benchmark_mask(b *testing.B) { } }, }, + { + name: "nbio", + fn: func(b *testing.B, key [4]byte, p []byte) { + keyb := key[:] + for i := 0; i < b.N; i++ { + nbioMaskBytes(p, keyb) + } + }, + }, } key := [4]byte{1, 2, 3, 4} - for _, size := range sizes { - p := make([]byte, size) + for _, fn := range fns { + b.Run(fn.name, func(b *testing.B) { + for _, size := range sizes { + p := make([]byte, size) - b.Run(strconv.Itoa(size), func(b *testing.B) { - for _, fn := range fns { - b.Run(fn.name, func(b *testing.B) { + b.Run(strconv.Itoa(size), func(b *testing.B) { b.SetBytes(int64(size)) fn.fn(b, key, p) diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index 3f32a416..f418d288 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -8,6 +8,7 @@ require ( github.com/gin-gonic/gin v1.9.1 github.com/gobwas/ws v1.3.0 github.com/gorilla/websocket v1.5.0 + github.com/lesismal/nbio v1.3.18 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) @@ -25,6 +26,7 @@ require ( github.com/json-iterator/go v1.1.12 // indirect github.com/klauspost/cpuid/v2 v2.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect + github.com/lesismal/llib v1.1.12 // indirect github.com/mattn/go-isatty v0.0.19 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/reflect2 v1.0.2 // indirect diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 47f324bb..658a4a7b 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -41,6 +41,10 @@ github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZX github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/leodido/go-urn v1.2.4 h1:XlAE/cm/ms7TE/VMVoduSpNBoyc2dOxHs5MZSwAN63Q= github.com/leodido/go-urn v1.2.4/go.mod h1:7ZrI8mTSeBSHl/UaRyKQW1qZeMgak41ANeCNaVckg+4= +github.com/lesismal/llib v1.1.12 h1:KJFB8bL02V+QGIvILEw/w7s6bKj9Ps9Px97MZP2EOk0= +github.com/lesismal/llib v1.1.12/go.mod h1:70tFXXe7P1FZ02AU9l8LgSOK7d7sRrpnkUr3rd3gKSg= +github.com/lesismal/nbio v1.3.18 h1:kmJZlxjQpVfuCPYcXdv0Biv9LHVViJZet5K99Xs3RAs= +github.com/lesismal/nbio v1.3.18/go.mod h1:KWlouFT5cgDdW5sMX8RsHASUMGniea9X0XIellZ0B38= github.com/mattn/go-isatty v0.0.19 h1:JITubQf0MOLdlGRuRq+jtsDlekdYPia9ZFsB8h/APPA= github.com/mattn/go-isatty v0.0.19/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= @@ -67,19 +71,51 @@ github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/ugorji/go/codec v1.2.11 h1:BMaWp1Bb6fHwEtbplGBGJ498wD+LKlNSl25MjdZY4dU= github.com/ugorji/go/codec v1.2.11/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.3.0 h1:02VY4/ZcO/gBOH6PUaoiptASxtXU10jazRCP865E97k= golang.org/x/arch v0.3.0/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= +golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= +golang.org/x/crypto v0.0.0-20210513122933-cd7d49e622d5/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8= +golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.9.0 h1:LF6fAI+IutBocDJ2OT0Q1g8plpYljMZ4+lty+dsqw3g= golang.org/x/crypto v0.9.0/go.mod h1:yrmDGqONDYtNj3tH8X9dzUun2m2lzPa9ngI6/RUPGR0= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= +golang.org/x/net v0.0.0-20210510120150-4163338589ed/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.10.0 h1:X2//UzNDwYmtCLn7To6G58Wr6f5ahEAQgKNzv9Y951M= golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= +golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= +golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= +golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= +golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= +golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.9.0 h1:2sjJmO8cDvYveuX97RDLsxlyUxLl+GHoLxBiRdHllBE= golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= +golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543 h1:E7g+9GITq07hpfrRu66IVDexMakfv52eLZ2CXBWiKr4= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= diff --git a/mask.go b/mask.go index 7c9fe308..b29435e9 100644 --- a/mask.go +++ b/mask.go @@ -2,6 +2,6 @@ package websocket -func mask(key uint32, b []byte) uint32 { - return maskGo(key, b) +func mask(b []byte, key uint32) uint32 { + return maskGo(b, key) } diff --git a/mask_amd64.s b/mask_amd64.s index 905d7e4a..73ae59b4 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -19,8 +19,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMPQ CX, $8 JL less_than_8 - CMPQ CX, $64 - JL less_than_64 CMPQ CX, $512 JLE sse TESTQ $31, AX diff --git a/mask_arm64.s b/mask_arm64.s index 741b77a5..8fd49aa9 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -4,8 +4,8 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 // R0 = b // R1 = len - // R2 = uint64(key)<<32 | uint64(key) // R3 = key (uint32) + // R2 = uint64(key)<<32 | uint64(key) MOVD b_ptr+0(FP), R0 MOVD b_len+8(FP), R1 MOVWU key+16(FP), R3 diff --git a/mask_asm.go b/mask_asm.go index 34021fa7..b8c4ee66 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,7 +2,7 @@ package websocket -func mask(key uint32, b []byte) uint32 { +func mask(b []byte, key uint32) uint32 { if len(b) > 0 { return maskAsm(&b[0], len(b), key) } diff --git a/read.go b/read.go index 8742842e..81b89831 100644 --- a/read.go +++ b/read.go @@ -289,7 +289,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { } if h.masked { - mask(h.maskKey, b) + mask(b, h.maskKey) } switch h.opcode { @@ -453,7 +453,7 @@ func (mr *msgReader) read(p []byte) (int, error) { mr.payloadLength -= int64(n) if !mr.c.client { - mr.maskKey = mask(mr.maskKey, p) + mr.maskKey = mask(p, mr.maskKey) } return n, nil diff --git a/write.go b/write.go index 7b1152ce..7ac7ce63 100644 --- a/write.go +++ b/write.go @@ -365,7 +365,7 @@ func (c *Conn) writeFramePayload(p []byte) (n int, err error) { return n, err } - maskKey = mask(maskKey, c.writeBuf[i:c.bw.Buffered()]) + maskKey = mask(c.writeBuf[i:c.bw.Buffered()], maskKey) p = p[j:] n += j From 369d641608eba0a8387f79eb204c56f364c2e31d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 17:04:52 -0700 Subject: [PATCH 123/152] mask_arm64.s: Cleanup --- mask_amd64.s | 4 ++-- mask_arm64.s | 24 +++++++++++------------- 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/mask_amd64.s b/mask_amd64.s index 73ae59b4..8464440b 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -117,10 +117,10 @@ less_than_4: less_than_2: TESTQ $1, CX - JZ done + JZ end XORB SI, (AX) ROLL $24, SI -done: +end: MOVL SI, ret+24(FP) RET diff --git a/mask_arm64.s b/mask_arm64.s index 8fd49aa9..42a1211f 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -15,7 +15,7 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMP $64, R1 BLT less_than_64 -// TODO: allign memory like amd64 +// TODO: align memory like amd64 loop_64: VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] @@ -29,41 +29,39 @@ loop_64: BGE loop_64 less_than_64: - // quick end - CBZ R1, end - TBZ $5, R1, less_than32 + TBZ $5, R1, less_than_32 VLD1 (R0), [V1.B16, V2.B16] VEOR V1.B16, V0.B16, V1.B16 VEOR V2.B16, V0.B16, V2.B16 VST1.P [V1.B16, V2.B16], 32(R0) -less_than32: - TBZ $4, R1, less_than16 +less_than_32: + TBZ $4, R1, less_than_16 LDP (R0), (R11, R12) EOR R11, R2, R11 EOR R12, R2, R12 STP.P (R11, R12), 16(R0) -less_than16: - TBZ $3, R1, less_than8 +less_than_16: + TBZ $3, R1, less_than_8 MOVD (R0), R11 EOR R2, R11, R11 MOVD.P R11, 8(R0) -less_than8: - TBZ $2, R1, less_than4 +less_than_8: + TBZ $2, R1, less_than_4 MOVWU (R0), R11 EORW R2, R11, R11 MOVWU.P R11, 4(R0) -less_than4: - TBZ $1, R1, less_than2 +less_than_4: + TBZ $1, R1, less_than_2 MOVHU (R0), R11 EORW R3, R11, R11 MOVHU.P R11, 2(R0) RORW $16, R3 -less_than2: +less_than_2: TBZ $0, R1, end MOVBU (R0), R11 EORW R3, R11, R11 From fb13df2dc30520f64bd4daa167ed9c7e739a98b7 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 19:46:00 -0700 Subject: [PATCH 124/152] ci/bench.sh: Benchmark masking on arm64 with QEMU --- ci/bench.sh | 3 +++ internal/thirdparty/frame_test.go | 5 +++++ 2 files changed, 8 insertions(+) diff --git a/ci/bench.sh b/ci/bench.sh index a553b93a..6af59ecf 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -6,4 +6,7 @@ go test --run=^$ --bench=. --benchmem --memprofile ci/out/prof.mem --cpuprofile ( cd ./internal/thirdparty go test --run=^$ --bench=. --benchmem --memprofile ../../ci/out/prof-thirdparty.mem --cpuprofile ../../ci/out/prof-thirdparty.cpu -o ../../ci/out/thirdparty.test "$@" . + + GOARCH=arm64 go test -c -o ../../ci/out/thirdparty-arm64.test . + qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem --test.memprofile ../../ci/out/prof-thirdparty-arm64.mem --test.cpuprofile ../../ci/out/prof-thirdparty-arm64.cpu . ) diff --git a/internal/thirdparty/frame_test.go b/internal/thirdparty/frame_test.go index 7202322d..89042e53 100644 --- a/internal/thirdparty/frame_test.go +++ b/internal/thirdparty/frame_test.go @@ -2,6 +2,7 @@ package thirdparty import ( "encoding/binary" + "runtime" "strconv" "testing" _ "unsafe" @@ -34,6 +35,10 @@ func nbioMaskBytes(b, key []byte) int func gorillaMaskBytes(key [4]byte, pos int, b []byte) int func Benchmark_mask(b *testing.B) { + b.Run(runtime.GOARCH, benchmark_mask) +} + +func benchmark_mask(b *testing.B) { sizes := []int{ 8, 16, From ecf7dec40098cbc3b659d99b467c0d6c97f38c6c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 19 Oct 2023 19:52:36 -0700 Subject: [PATCH 125/152] ci/bench.sh: Install QEMU on CI --- README.md | 3 +-- ci/bench.sh | 5 +++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 4b2d828e..0f286e63 100644 --- a/README.md +++ b/README.md @@ -120,9 +120,8 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [3x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go +- [4x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) diff --git a/ci/bench.sh b/ci/bench.sh index 6af59ecf..afc2d825 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -8,5 +8,10 @@ go test --run=^$ --bench=. --benchmem --memprofile ci/out/prof.mem --cpuprofile go test --run=^$ --bench=. --benchmem --memprofile ../../ci/out/prof-thirdparty.mem --cpuprofile ../../ci/out/prof-thirdparty.cpu -o ../../ci/out/thirdparty.test "$@" . GOARCH=arm64 go test -c -o ../../ci/out/thirdparty-arm64.test . + if [ "${CI-}" ]; then + sudo apt-get update + sudo apt-get install -y qemu-user-static + alias qemu-aarch64=qemu-aarch64-static + fi qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem --test.memprofile ../../ci/out/prof-thirdparty-arm64.mem --test.cpuprofile ../../ci/out/prof-thirdparty-arm64.cpu . ) From d34e5d4b8e4c3d9ab9311d43492fe0bded560ccc Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 07:47:25 -0700 Subject: [PATCH 126/152] wsjson: Add json.Encoder vs json.Marshal benchmark json.Encoder is 42% faster than json.Marshal thanks to the memory reuse. goos: linux goarch: amd64 pkg: nhooyr.io/websocket/wsjson cpu: 12th Gen Intel(R) Core(TM) i5-1235U BenchmarkJSON/json.Encoder-12 3517579 340.2 ns/op 24 B/op 1 allocs/op BenchmarkJSON/json.Marshal-12 2374086 484.3 ns/op 728 B/op 2 allocs/op Closes #409 --- wsjson/wsjson_test.go | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 wsjson/wsjson_test.go diff --git a/wsjson/wsjson_test.go b/wsjson/wsjson_test.go new file mode 100644 index 00000000..a70e808c --- /dev/null +++ b/wsjson/wsjson_test.go @@ -0,0 +1,24 @@ +package wsjson_test + +import ( + "encoding/json" + "io" + "strings" + "testing" +) + +func BenchmarkJSON(b *testing.B) { + msg := []byte(strings.Repeat("1234", 128)) + b.SetBytes(int64(len(msg))) + b.ReportAllocs() + b.Run("json.Encoder", func(b *testing.B) { + for i := 0; i < b.N; i++ { + json.NewEncoder(io.Discard).Encode(msg) + } + }) + b.Run("json.Marshal", func(b *testing.B) { + for i := 0; i < b.N; i++ { + json.Marshal(msg) + } + }) +} From e25d9681bd6cc0a9176a2fa35d1a1e16bdcd685d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 07:55:15 -0700 Subject: [PATCH 127/152] ci/bench.sh: Don't profile by default --- ci/bench.sh | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/ci/bench.sh b/ci/bench.sh index afc2d825..afe0490b 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -2,16 +2,19 @@ set -eu cd -- "$(dirname "$0")/.." -go test --run=^$ --bench=. --benchmem --memprofile ci/out/prof.mem --cpuprofile ci/out/prof.cpu -o ci/out/websocket.test "$@" . +go test --run=^$ --bench=. --benchmem "$@" ./... +# For profiling add: --memprofile ci/out/prof.mem --cpuprofile ci/out/prof.cpu -o ci/out/websocket.test ( cd ./internal/thirdparty - go test --run=^$ --bench=. --benchmem --memprofile ../../ci/out/prof-thirdparty.mem --cpuprofile ../../ci/out/prof-thirdparty.cpu -o ../../ci/out/thirdparty.test "$@" . + go test --run=^$ --bench=. --benchmem "$@" . - GOARCH=arm64 go test -c -o ../../ci/out/thirdparty-arm64.test . - if [ "${CI-}" ]; then - sudo apt-get update - sudo apt-get install -y qemu-user-static - alias qemu-aarch64=qemu-aarch64-static + GOARCH=arm64 go test -c -o ../../ci/out/thirdparty-arm64.test "$@" . + if [ "$#" -eq 0 ]; then + if [ "${CI-}" ]; then + sudo apt-get update + sudo apt-get install -y qemu-user-static + alias qemu-aarch64=qemu-aarch64-static + fi + qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem fi - qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem --test.memprofile ../../ci/out/prof-thirdparty-arm64.mem --test.cpuprofile ../../ci/out/prof-thirdparty-arm64.cpu . ) From 640e3c25bcb716956df6452b85ef7ead53477040 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 07:56:39 -0700 Subject: [PATCH 128/152] ci/bench.sh: Try function instead of alias --- ci/bench.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/bench.sh b/ci/bench.sh index afe0490b..5b1360d0 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -13,7 +13,7 @@ go test --run=^$ --bench=. --benchmem "$@" ./... if [ "${CI-}" ]; then sudo apt-get update sudo apt-get install -y qemu-user-static - alias qemu-aarch64=qemu-aarch64-static + qemu-aarch64() { qemu-aarch64-static "$@" } fi qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem fi From 0596e7a0e19d842ac2d9db0d598902dba6485cc4 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 08:04:36 -0700 Subject: [PATCH 129/152] wsjson: Extend benchmark with multiple sizes [qrvnl@dios ~/src/websocket] 130$ go test -bench=. ./wsjson/ goos: linux goarch: amd64 pkg: nhooyr.io/websocket/wsjson cpu: 12th Gen Intel(R) Core(TM) i5-1235U BenchmarkJSON/json.Encoder/8-12 14041426 72.59 ns/op 110.21 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/16-12 13936426 86.99 ns/op 183.92 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/32-12 11416401 115.3 ns/op 277.59 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/128-12 4600574 264.7 ns/op 483.55 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/256-12 2710398 433.9 ns/op 590.06 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/512-12 1588930 717.3 ns/op 713.82 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/1024-12 823138 1484 ns/op 689.80 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/2048-12 402823 2875 ns/op 712.32 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/4096-12 213926 5602 ns/op 731.14 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/8192-12 92864 11281 ns/op 726.19 MB/s 16 B/op 1 allocs/op BenchmarkJSON/json.Encoder/16384-12 39318 29203 ns/op 561.04 MB/s 19 B/op 1 allocs/op BenchmarkJSON/json.Marshal/8-12 10768671 114.5 ns/op 69.89 MB/s 48 B/op 2 allocs/op BenchmarkJSON/json.Marshal/16-12 10140996 113.9 ns/op 140.51 MB/s 64 B/op 2 allocs/op BenchmarkJSON/json.Marshal/32-12 9211780 121.6 ns/op 263.06 MB/s 64 B/op 2 allocs/op BenchmarkJSON/json.Marshal/128-12 4632796 264.2 ns/op 484.53 MB/s 224 B/op 2 allocs/op BenchmarkJSON/json.Marshal/256-12 2441511 473.5 ns/op 540.65 MB/s 432 B/op 2 allocs/op BenchmarkJSON/json.Marshal/512-12 1298788 896.2 ns/op 571.27 MB/s 912 B/op 2 allocs/op BenchmarkJSON/json.Marshal/1024-12 602084 1866 ns/op 548.83 MB/s 1808 B/op 2 allocs/op BenchmarkJSON/json.Marshal/2048-12 341151 3817 ns/op 536.61 MB/s 3474 B/op 2 allocs/op BenchmarkJSON/json.Marshal/4096-12 175594 7034 ns/op 582.32 MB/s 6548 B/op 2 allocs/op BenchmarkJSON/json.Marshal/8192-12 83222 15023 ns/op 545.30 MB/s 13591 B/op 2 allocs/op BenchmarkJSON/json.Marshal/16384-12 33087 39348 ns/op 416.39 MB/s 27304 B/op 2 allocs/op PASS ok nhooyr.io/websocket/wsjson 32.934s --- wsjson/wsjson_test.go | 45 +++++++++++++++++++++++++++++++++++-------- 1 file changed, 37 insertions(+), 8 deletions(-) diff --git a/wsjson/wsjson_test.go b/wsjson/wsjson_test.go index a70e808c..080ab38d 100644 --- a/wsjson/wsjson_test.go +++ b/wsjson/wsjson_test.go @@ -3,22 +3,51 @@ package wsjson_test import ( "encoding/json" "io" - "strings" + "strconv" "testing" + + "nhooyr.io/websocket/internal/test/xrand" ) func BenchmarkJSON(b *testing.B) { - msg := []byte(strings.Repeat("1234", 128)) - b.SetBytes(int64(len(msg))) - b.ReportAllocs() + sizes := []int{ + 8, + 16, + 32, + 128, + 256, + 512, + 1024, + 2048, + 4096, + 8192, + 16384, + } + b.Run("json.Encoder", func(b *testing.B) { - for i := 0; i < b.N; i++ { - json.NewEncoder(io.Discard).Encode(msg) + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + msg := xrand.String(size) + b.SetBytes(int64(size)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + json.NewEncoder(io.Discard).Encode(msg) + } + }) } }) b.Run("json.Marshal", func(b *testing.B) { - for i := 0; i < b.N; i++ { - json.Marshal(msg) + for _, size := range sizes { + b.Run(strconv.Itoa(size), func(b *testing.B) { + msg := xrand.String(size) + b.SetBytes(int64(size)) + b.ReportAllocs() + b.ResetTimer() + for i := 0; i < b.N; i++ { + json.Marshal(msg) + } + }) } }) } From 30447a3e05b34bbf55ed531aebeb31192dacd251 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 08:08:21 -0700 Subject: [PATCH 130/152] ci/bench.sh: Just symlink the expected qemu-aarch64 binary name --- ci/bench.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ci/bench.sh b/ci/bench.sh index 5b1360d0..30c06986 100755 --- a/ci/bench.sh +++ b/ci/bench.sh @@ -13,7 +13,7 @@ go test --run=^$ --bench=. --benchmem "$@" ./... if [ "${CI-}" ]; then sudo apt-get update sudo apt-get install -y qemu-user-static - qemu-aarch64() { qemu-aarch64-static "$@" } + ln -s /usr/bin/qemu-aarch64-static /usr/local/bin/qemu-aarch64 fi qemu-aarch64 ../../ci/out/thirdparty-arm64.test --test.run=^$ --test.bench=Benchmark_mask --test.benchmem fi From f4e61e5a124dfdc0df65ae401b6f7c134005fc5f Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 20 Oct 2023 21:45:28 -0700 Subject: [PATCH 131/152] ci/fmt.sh: Error if changes on CI --- ci/fmt.sh | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/ci/fmt.sh b/ci/fmt.sh index 6e5a68e4..31d0c15d 100755 --- a/ci/fmt.sh +++ b/ci/fmt.sh @@ -18,3 +18,7 @@ npx prettier@3.0.3 \ $(git ls-files "*.yml" "*.md" "*.js" "*.css" "*.html") go run golang.org/x/tools/cmd/stringer@latest -type=opcode,MessageType,StatusCode -output=stringer.go + +if [ "${CI-}" ]; then + git diff --exit-code +fi From f533f430c7d63e9e0bceb2dcbbd5d75602803b82 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sat, 21 Oct 2023 05:45:11 -0700 Subject: [PATCH 132/152] mask.go: Reorganize --- frame.go | 125 ------------------------------------------------ mask.go | 131 +++++++++++++++++++++++++++++++++++++++++++++++++-- mask_amd64.s | 36 +++++++++++--- mask_asm.go | 2 + mask_go.go | 7 +++ 5 files changed, 166 insertions(+), 135 deletions(-) create mode 100644 mask_go.go diff --git a/frame.go b/frame.go index 5d826ea3..d5631863 100644 --- a/frame.go +++ b/frame.go @@ -8,7 +8,6 @@ import ( "fmt" "io" "math" - "math/bits" "nhooyr.io/websocket/internal/errd" ) @@ -172,127 +171,3 @@ func writeFrameHeader(h header, w *bufio.Writer, buf []byte) (err error) { return nil } - -// maskGo applies the WebSocket masking algorithm to p -// with the given key. -// See https://tools.ietf.org/html/rfc6455#section-5.3 -// -// The returned value is the correctly rotated key to -// to continue to mask/unmask the message. -// -// It is optimized for LittleEndian and expects the key -// to be in little endian. -// -// See https://github.com/golang/go/issues/31586 -// -//lint:ignore U1000 mask.go -func maskGo(b []byte, key uint32) uint32 { - if len(b) >= 8 { - key64 := uint64(key)<<32 | uint64(key) - - // At some point in the future we can clean these unrolled loops up. - // See https://github.com/golang/go/issues/31586#issuecomment-487436401 - - // Then we xor until b is less than 128 bytes. - for len(b) >= 128 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - v = binary.LittleEndian.Uint64(b[64:72]) - binary.LittleEndian.PutUint64(b[64:72], v^key64) - v = binary.LittleEndian.Uint64(b[72:80]) - binary.LittleEndian.PutUint64(b[72:80], v^key64) - v = binary.LittleEndian.Uint64(b[80:88]) - binary.LittleEndian.PutUint64(b[80:88], v^key64) - v = binary.LittleEndian.Uint64(b[88:96]) - binary.LittleEndian.PutUint64(b[88:96], v^key64) - v = binary.LittleEndian.Uint64(b[96:104]) - binary.LittleEndian.PutUint64(b[96:104], v^key64) - v = binary.LittleEndian.Uint64(b[104:112]) - binary.LittleEndian.PutUint64(b[104:112], v^key64) - v = binary.LittleEndian.Uint64(b[112:120]) - binary.LittleEndian.PutUint64(b[112:120], v^key64) - v = binary.LittleEndian.Uint64(b[120:128]) - binary.LittleEndian.PutUint64(b[120:128], v^key64) - b = b[128:] - } - - // Then we xor until b is less than 64 bytes. - for len(b) >= 64 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - v = binary.LittleEndian.Uint64(b[32:40]) - binary.LittleEndian.PutUint64(b[32:40], v^key64) - v = binary.LittleEndian.Uint64(b[40:48]) - binary.LittleEndian.PutUint64(b[40:48], v^key64) - v = binary.LittleEndian.Uint64(b[48:56]) - binary.LittleEndian.PutUint64(b[48:56], v^key64) - v = binary.LittleEndian.Uint64(b[56:64]) - binary.LittleEndian.PutUint64(b[56:64], v^key64) - b = b[64:] - } - - // Then we xor until b is less than 32 bytes. - for len(b) >= 32 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - v = binary.LittleEndian.Uint64(b[16:24]) - binary.LittleEndian.PutUint64(b[16:24], v^key64) - v = binary.LittleEndian.Uint64(b[24:32]) - binary.LittleEndian.PutUint64(b[24:32], v^key64) - b = b[32:] - } - - // Then we xor until b is less than 16 bytes. - for len(b) >= 16 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - v = binary.LittleEndian.Uint64(b[8:16]) - binary.LittleEndian.PutUint64(b[8:16], v^key64) - b = b[16:] - } - - // Then we xor until b is less than 8 bytes. - for len(b) >= 8 { - v := binary.LittleEndian.Uint64(b) - binary.LittleEndian.PutUint64(b, v^key64) - b = b[8:] - } - } - - // Then we xor until b is less than 4 bytes. - for len(b) >= 4 { - v := binary.LittleEndian.Uint32(b) - binary.LittleEndian.PutUint32(b, v^key) - b = b[4:] - } - - // xor remaining bytes. - for i := range b { - b[i] ^= byte(key) - key = bits.RotateLeft32(key, -8) - } - - return key -} diff --git a/mask.go b/mask.go index b29435e9..5f0746dc 100644 --- a/mask.go +++ b/mask.go @@ -1,7 +1,130 @@ -//go:build !amd64 && !arm64 && !js - package websocket -func mask(b []byte, key uint32) uint32 { - return maskGo(b, key) +import ( + "encoding/binary" + "math/bits" +) + +// maskGo applies the WebSocket masking algorithm to p +// with the given key. +// See https://tools.ietf.org/html/rfc6455#section-5.3 +// +// The returned value is the correctly rotated key to +// to continue to mask/unmask the message. +// +// It is optimized for LittleEndian and expects the key +// to be in little endian. +// +// See https://github.com/golang/go/issues/31586 +// +//lint:ignore U1000 mask.go +func maskGo(b []byte, key uint32) uint32 { + if len(b) >= 8 { + key64 := uint64(key)<<32 | uint64(key) + + // At some point in the future we can clean these unrolled loops up. + // See https://github.com/golang/go/issues/31586#issuecomment-487436401 + + // Then we xor until b is less than 128 bytes. + for len(b) >= 128 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + v = binary.LittleEndian.Uint64(b[32:40]) + binary.LittleEndian.PutUint64(b[32:40], v^key64) + v = binary.LittleEndian.Uint64(b[40:48]) + binary.LittleEndian.PutUint64(b[40:48], v^key64) + v = binary.LittleEndian.Uint64(b[48:56]) + binary.LittleEndian.PutUint64(b[48:56], v^key64) + v = binary.LittleEndian.Uint64(b[56:64]) + binary.LittleEndian.PutUint64(b[56:64], v^key64) + v = binary.LittleEndian.Uint64(b[64:72]) + binary.LittleEndian.PutUint64(b[64:72], v^key64) + v = binary.LittleEndian.Uint64(b[72:80]) + binary.LittleEndian.PutUint64(b[72:80], v^key64) + v = binary.LittleEndian.Uint64(b[80:88]) + binary.LittleEndian.PutUint64(b[80:88], v^key64) + v = binary.LittleEndian.Uint64(b[88:96]) + binary.LittleEndian.PutUint64(b[88:96], v^key64) + v = binary.LittleEndian.Uint64(b[96:104]) + binary.LittleEndian.PutUint64(b[96:104], v^key64) + v = binary.LittleEndian.Uint64(b[104:112]) + binary.LittleEndian.PutUint64(b[104:112], v^key64) + v = binary.LittleEndian.Uint64(b[112:120]) + binary.LittleEndian.PutUint64(b[112:120], v^key64) + v = binary.LittleEndian.Uint64(b[120:128]) + binary.LittleEndian.PutUint64(b[120:128], v^key64) + b = b[128:] + } + + // Then we xor until b is less than 64 bytes. + for len(b) >= 64 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + v = binary.LittleEndian.Uint64(b[32:40]) + binary.LittleEndian.PutUint64(b[32:40], v^key64) + v = binary.LittleEndian.Uint64(b[40:48]) + binary.LittleEndian.PutUint64(b[40:48], v^key64) + v = binary.LittleEndian.Uint64(b[48:56]) + binary.LittleEndian.PutUint64(b[48:56], v^key64) + v = binary.LittleEndian.Uint64(b[56:64]) + binary.LittleEndian.PutUint64(b[56:64], v^key64) + b = b[64:] + } + + // Then we xor until b is less than 32 bytes. + for len(b) >= 32 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + v = binary.LittleEndian.Uint64(b[16:24]) + binary.LittleEndian.PutUint64(b[16:24], v^key64) + v = binary.LittleEndian.Uint64(b[24:32]) + binary.LittleEndian.PutUint64(b[24:32], v^key64) + b = b[32:] + } + + // Then we xor until b is less than 16 bytes. + for len(b) >= 16 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + v = binary.LittleEndian.Uint64(b[8:16]) + binary.LittleEndian.PutUint64(b[8:16], v^key64) + b = b[16:] + } + + // Then we xor until b is less than 8 bytes. + for len(b) >= 8 { + v := binary.LittleEndian.Uint64(b) + binary.LittleEndian.PutUint64(b, v^key64) + b = b[8:] + } + } + + // Then we xor until b is less than 4 bytes. + for len(b) >= 4 { + v := binary.LittleEndian.Uint32(b) + binary.LittleEndian.PutUint32(b, v^key) + b = b[4:] + } + + // xor remaining bytes. + for i := range b { + b[i] ^= byte(key) + key = bits.RotateLeft32(key, -8) + } + + return key } diff --git a/mask_amd64.s b/mask_amd64.s index 8464440b..ba37731d 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -19,11 +19,16 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMPQ CX, $8 JL less_than_8 - CMPQ CX, $512 + CMPQ CX, $128 JLE sse TESTQ $31, AX JNZ unaligned +aligned: + CMPB ·useAVX2(SB), $1 + JE avx2 + JMP sse + unaligned_loop_1byte: XORB SI, (AX) INCQ AX @@ -40,7 +45,7 @@ unaligned_loop_1byte: ORQ DX, DI TESTQ $31, AX - JZ sse + JZ aligned unaligned: // $7 & len, if not zero jump to loop_1b. @@ -54,8 +59,27 @@ unaligned_loop: SUBQ $8, CX TESTQ $31, AX JNZ unaligned_loop - JMP sse - + JMP aligned + +avx2: + CMPQ CX, $128 + JL sse + VMOVQ DI, X0 + VPBROADCASTQ X0, Y0 + +// TODO: shouldn't these be aligned movs now? +// TODO: should be 256? +avx2_loop: + VMOVDQU (AX), Y1 + VPXOR Y0, Y1, Y2 + VMOVDQU Y2, (AX) + ADDQ $128, AX + SUBQ $128, CX + CMPQ CX, $128 + // Loop if CX >= 128. + JAE avx2_loop + +// TODO: should be 128? sse: CMPQ CX, $64 JL less_than_64 @@ -63,8 +87,8 @@ sse: PUNPCKLQDQ X0, X0 sse_loop: - MOVOU 0*16(AX), X1 - MOVOU 1*16(AX), X2 + MOVOU (AX), X1 + MOVOU 16(AX), X2 MOVOU 2*16(AX), X3 MOVOU 3*16(AX), X4 PXOR X0, X1 diff --git a/mask_asm.go b/mask_asm.go index b8c4ee66..1f294982 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -9,5 +9,7 @@ func mask(b []byte, key uint32) uint32 { return key } +var useAVX2 = true + //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 diff --git a/mask_go.go b/mask_go.go new file mode 100644 index 00000000..b29435e9 --- /dev/null +++ b/mask_go.go @@ -0,0 +1,7 @@ +//go:build !amd64 && !arm64 && !js + +package websocket + +func mask(b []byte, key uint32) uint32 { + return maskGo(b, key) +} From a1bb44194159a5ff19ddb3032b796d8466d64d7a Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 6 Feb 2024 16:59:47 -0800 Subject: [PATCH 133/152] ci: Fix dev coverage output --- .github/workflows/daily.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index b1e64fbc..340de501 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -50,5 +50,5 @@ jobs: - run: AUTOBAHN=1 ./ci/test.sh - uses: actions/upload-artifact@v3 with: - name: coverage.html + name: coverage-dev.html path: ./ci/out/coverage.html From fee373961a0522e40495f989bdd408146390f7e0 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Wed, 7 Feb 2024 02:42:51 -0800 Subject: [PATCH 134/152] mask_asm: Note implementation may not be perfect --- go.mod | 2 ++ go.sum | 2 ++ mask_arm64.s | 3 +-- mask_asm.go | 9 ++++++++- 4 files changed, 13 insertions(+), 3 deletions(-) diff --git a/go.mod b/go.mod index 715a9f7a..dbc4a5a7 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module nhooyr.io/websocket go 1.19 + +require golang.org/x/sys v0.17.0 // indirect diff --git a/go.sum b/go.sum index e69de29b..735d9a79 100644 --- a/go.sum +++ b/go.sum @@ -0,0 +1,2 @@ +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/mask_arm64.s b/mask_arm64.s index 42a1211f..e494b43a 100644 --- a/mask_arm64.s +++ b/mask_arm64.s @@ -15,8 +15,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 CMP $64, R1 BLT less_than_64 -// TODO: align memory like amd64 - loop_64: VLD1 (R0), [V1.B16, V2.B16, V3.B16, V4.B16] VEOR V1.B16, V0.B16, V1.B16 @@ -29,6 +27,7 @@ loop_64: BGE loop_64 less_than_64: + CBZ R1, end TBZ $5, R1, less_than_32 VLD1 (R0), [V1.B16, V2.B16] VEOR V1.B16, V0.B16, V1.B16 diff --git a/mask_asm.go b/mask_asm.go index 1f294982..865cd4b8 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,6 +2,8 @@ package websocket +import "golang.org/x/sys/cpu" + func mask(b []byte, key uint32) uint32 { if len(b) > 0 { return maskAsm(&b[0], len(b), key) @@ -9,7 +11,12 @@ func mask(b []byte, key uint32) uint32 { return key } -var useAVX2 = true +var useAVX2 = cpu.X86.HasAVX2 +// @nhooyr: I am not confident that the amd64 or the arm64 implementations of this +// function are perfect. There are almost certainly missing optimizations or +// opportunities for // simplification. I'm confident there are no bugs though. +// For example, the arm64 implementation doesn't align memory like the amd64. +// Or the amd64 implementation could use AVX512 instead of just AVX2. //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From 68fc887a3af8be45880f95f2a5f249a84b2b99b8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 04:51:47 -0800 Subject: [PATCH 135/152] mask.go: Revert my changes I'm just not good enough at assembly. I added tests to confirm that @wdvxdr's implementation works correctly and matches the output of the basic masking loop. --- go.mod | 2 +- internal/examples/go.mod | 2 ++ internal/examples/go.sum | 2 ++ internal/thirdparty/go.mod | 2 +- internal/thirdparty/go.sum | 4 +-- mask_amd64.s | 62 ++++++++++++++++---------------- mask_asm.go | 2 ++ mask_asm_test.go | 11 ++++++ mask_test.go | 73 ++++++++++++++++++++++++++++++++++++++ 9 files changed, 126 insertions(+), 34 deletions(-) create mode 100644 mask_asm_test.go create mode 100644 mask_test.go diff --git a/go.mod b/go.mod index dbc4a5a7..c6ec72cc 100644 --- a/go.mod +++ b/go.mod @@ -2,4 +2,4 @@ module nhooyr.io/websocket go 1.19 -require golang.org/x/sys v0.17.0 // indirect +require golang.org/x/sys v0.17.0 diff --git a/internal/examples/go.mod b/internal/examples/go.mod index c98b81ce..50695945 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -8,3 +8,5 @@ require ( golang.org/x/time v0.3.0 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) + +require golang.org/x/sys v0.17.0 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index f8a07e82..06068548 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,2 +1,4 @@ +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/thirdparty/go.mod b/internal/thirdparty/go.mod index f418d288..d991dd64 100644 --- a/internal/thirdparty/go.mod +++ b/internal/thirdparty/go.mod @@ -36,7 +36,7 @@ require ( golang.org/x/arch v0.3.0 // indirect golang.org/x/crypto v0.9.0 // indirect golang.org/x/net v0.10.0 // indirect - golang.org/x/sys v0.13.0 // indirect + golang.org/x/sys v0.17.0 // indirect golang.org/x/text v0.9.0 // indirect google.golang.org/protobuf v1.30.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/internal/thirdparty/go.sum b/internal/thirdparty/go.sum index 658a4a7b..1f542103 100644 --- a/internal/thirdparty/go.sum +++ b/internal/thirdparty/go.sum @@ -100,8 +100,8 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.13.0 h1:Af8nKPmuFypiUBjVoU9V20FiaFXOcuZI21p0ycVYYGE= -golang.org/x/sys v0.13.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= +golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= diff --git a/mask_amd64.s b/mask_amd64.s index ba37731d..caca53ec 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -10,15 +10,17 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 MOVQ len+8(FP), CX MOVL key+16(FP), SI - // Calculate the DI aka the uint64 key. - // DI = uint64(SI) | uint64(SI)<<32 + // calculate the DI + // DI = SI<<32 | SI MOVL SI, DI MOVQ DI, DX SHLQ $32, DI ORQ DX, DI - CMPQ CX, $8 - JL less_than_8 + CMPQ CX, $15 + JLE less_than_16 + CMPQ CX, $63 + JLE less_than_64 CMPQ CX, $128 JLE sse TESTQ $31, AX @@ -37,8 +39,8 @@ unaligned_loop_1byte: TESTQ $7, AX JNZ unaligned_loop_1byte - // Calculate DI again since SI was modified. - // DI = uint64(SI) | uint64(SI)<<32 + // calculate DI again since SI was modified + // DI = SI<<32 | SI MOVL SI, DI MOVQ DI, DX SHLQ $32, DI @@ -48,12 +50,11 @@ unaligned_loop_1byte: JZ aligned unaligned: - // $7 & len, if not zero jump to loop_1b. - TESTQ $7, AX + TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. JNZ unaligned_loop_1byte unaligned_loop: - // We don't need to check the CX since we know it's above 512. + // we don't need to check the CX since we know it's above 128 XORQ DI, (AX) ADDQ $8, AX SUBQ $8, CX @@ -62,33 +63,34 @@ unaligned_loop: JMP aligned avx2: - CMPQ CX, $128 + CMPQ CX, $0x80 JL sse VMOVQ DI, X0 VPBROADCASTQ X0, Y0 -// TODO: shouldn't these be aligned movs now? -// TODO: should be 256? avx2_loop: - VMOVDQU (AX), Y1 - VPXOR Y0, Y1, Y2 - VMOVDQU Y2, (AX) - ADDQ $128, AX - SUBQ $128, CX - CMPQ CX, $128 - // Loop if CX >= 128. - JAE avx2_loop - -// TODO: should be 128? + VPXOR (AX), Y0, Y1 + VPXOR 32(AX), Y0, Y2 + VPXOR 64(AX), Y0, Y3 + VPXOR 96(AX), Y0, Y4 + VMOVDQU Y1, (AX) + VMOVDQU Y2, 32(AX) + VMOVDQU Y3, 64(AX) + VMOVDQU Y4, 96(AX) + ADDQ $0x80, AX + SUBQ $0x80, CX + CMPQ CX, $0x80 + JAE avx2_loop // loop if CX >= 0x80 + sse: - CMPQ CX, $64 + CMPQ CX, $0x40 JL less_than_64 MOVQ DI, X0 PUNPCKLQDQ X0, X0 sse_loop: - MOVOU (AX), X1 - MOVOU 16(AX), X2 + MOVOU 0*16(AX), X1 + MOVOU 1*16(AX), X2 MOVOU 2*16(AX), X3 MOVOU 3*16(AX), X4 PXOR X0, X1 @@ -99,9 +101,9 @@ sse_loop: MOVOU X2, 1*16(AX) MOVOU X3, 2*16(AX) MOVOU X4, 3*16(AX) - ADDQ $64, AX - SUBQ $64, CX - CMPQ CX, $64 + ADDQ $0x40, AX + SUBQ $0x40, CX + CMPQ CX, $0x40 JAE sse_loop less_than_64: @@ -141,10 +143,10 @@ less_than_4: less_than_2: TESTQ $1, CX - JZ end + JZ done XORB SI, (AX) ROLL $24, SI -end: +done: MOVL SI, ret+24(FP) RET diff --git a/mask_asm.go b/mask_asm.go index 865cd4b8..bf4bb635 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -11,6 +11,7 @@ func mask(b []byte, key uint32) uint32 { return key } +//lint:ignore U1000 mask_*.s var useAVX2 = cpu.X86.HasAVX2 // @nhooyr: I am not confident that the amd64 or the arm64 implementations of this @@ -18,5 +19,6 @@ var useAVX2 = cpu.X86.HasAVX2 // opportunities for // simplification. I'm confident there are no bugs though. // For example, the arm64 implementation doesn't align memory like the amd64. // Or the amd64 implementation could use AVX512 instead of just AVX2. +// //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 diff --git a/mask_asm_test.go b/mask_asm_test.go new file mode 100644 index 00000000..416cbc43 --- /dev/null +++ b/mask_asm_test.go @@ -0,0 +1,11 @@ +//go:build amd64 || arm64 + +package websocket + +import "testing" + +func TestMaskASM(t *testing.T) { + t.Parallel() + + testMask(t, "maskASM", mask) +} diff --git a/mask_test.go b/mask_test.go new file mode 100644 index 00000000..5c3d43c4 --- /dev/null +++ b/mask_test.go @@ -0,0 +1,73 @@ +package websocket + +import ( + "bytes" + "crypto/rand" + "encoding/binary" + "math/big" + "math/bits" + "testing" + + "nhooyr.io/websocket/internal/test/assert" +) + +func basicMask(b []byte, key uint32) uint32 { + for i := range b { + b[i] ^= byte(key) + key = bits.RotateLeft32(key, -8) + } + return key +} + +func basicMask2(b []byte, key uint32) uint32 { + keyb := binary.LittleEndian.AppendUint32(nil, key) + pos := 0 + for i := range b { + b[i] ^= keyb[pos&3] + pos++ + } + return bits.RotateLeft32(key, (pos&3)*-8) +} + +func TestMask(t *testing.T) { + t.Parallel() + + testMask(t, "basicMask", basicMask) + testMask(t, "maskGo", maskGo) + testMask(t, "basicMask2", basicMask2) +} + +func testMask(t *testing.T, name string, fn func(b []byte, key uint32) uint32) { + t.Run(name, func(t *testing.T) { + t.Parallel() + for i := 0; i < 9999; i++ { + keyb := make([]byte, 4) + _, err := rand.Read(keyb) + assert.Success(t, err) + key := binary.LittleEndian.Uint32(keyb) + + n, err := rand.Int(rand.Reader, big.NewInt(1<<16)) + assert.Success(t, err) + + b := make([]byte, 1+n.Int64()) + _, err = rand.Read(b) + assert.Success(t, err) + + b2 := make([]byte, len(b)) + copy(b2, b) + b3 := make([]byte, len(b)) + copy(b3, b) + + key2 := basicMask(b2, key) + key3 := fn(b3, key) + + if key2 != key3 { + t.Errorf("expected key %X but got %X", key2, key3) + } + if !bytes.Equal(b2, b3) { + t.Error("bad bytes") + return + } + } + }) +} From f62cef395d5228b955967e05a7e7cbf5a0ab8f93 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 05:00:41 -0800 Subject: [PATCH 136/152] test.sh: Test assembly masking on arm64 --- ci/test.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/ci/test.sh b/ci/test.sh index 83bb9832..a3007614 100755 --- a/ci/test.sh +++ b/ci/test.sh @@ -11,6 +11,19 @@ cd -- "$(dirname "$0")/.." go test "$@" ./... ) +( + GOARCH=arm64 go test -c -o ./ci/out/websocket-arm64.test "$@" . + if [ "$#" -eq 0 ]; then + if [ "${CI-}" ]; then + sudo apt-get update + sudo apt-get install -y qemu-user-static + ln -s /usr/bin/qemu-aarch64-static /usr/local/bin/qemu-aarch64 + fi + qemu-aarch64 ./ci/out/websocket-arm64.test -test.run=TestMask + fi +) + + go install github.com/agnivade/wasmbrowsertest@latest go test --race --bench=. --timeout=1h --covermode=atomic --coverprofile=ci/out/coverage.prof --coverpkg=./... "$@" ./... sed -i.bak '/stringer\.go/d' ci/out/coverage.prof From 92acb74883ce505cd4eefd32841ef807de3e78f8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 05:06:05 -0800 Subject: [PATCH 137/152] internal/xcpu: Vendor golang.org/x/sys/cpu Standard library does this too. Unfortunate wish they just exposed it in the standard library. Perhaps we can isolate the specific code we need later. --- go.mod | 2 - go.sum | 2 - internal/examples/go.mod | 2 - internal/examples/go.sum | 2 - internal/xcpu/.gitattributes | 10 + internal/xcpu/.gitignore | 2 + internal/xcpu/README.md | 3 + internal/xcpu/asm_aix_ppc64.s | 17 ++ internal/xcpu/byteorder.go | 66 ++++++ internal/xcpu/cpu.go | 290 ++++++++++++++++++++++++++ internal/xcpu/cpu_aix.go | 33 +++ internal/xcpu/cpu_arm.go | 73 +++++++ internal/xcpu/cpu_arm64.go | 172 +++++++++++++++ internal/xcpu/cpu_arm64.s | 31 +++ internal/xcpu/cpu_gc_arm64.go | 11 + internal/xcpu/cpu_gc_s390x.go | 21 ++ internal/xcpu/cpu_gc_x86.go | 15 ++ internal/xcpu/cpu_gccgo_arm64.go | 11 + internal/xcpu/cpu_gccgo_s390x.go | 22 ++ internal/xcpu/cpu_gccgo_x86.c | 37 ++++ internal/xcpu/cpu_gccgo_x86.go | 31 +++ internal/xcpu/cpu_linux.go | 15 ++ internal/xcpu/cpu_linux_arm.go | 39 ++++ internal/xcpu/cpu_linux_arm64.go | 111 ++++++++++ internal/xcpu/cpu_linux_mips64x.go | 22 ++ internal/xcpu/cpu_linux_noinit.go | 9 + internal/xcpu/cpu_linux_ppc64x.go | 30 +++ internal/xcpu/cpu_linux_s390x.go | 40 ++++ internal/xcpu/cpu_loong64.go | 12 ++ internal/xcpu/cpu_mips64x.go | 15 ++ internal/xcpu/cpu_mipsx.go | 11 + internal/xcpu/cpu_netbsd_arm64.go | 173 +++++++++++++++ internal/xcpu/cpu_openbsd_arm64.go | 65 ++++++ internal/xcpu/cpu_openbsd_arm64.s | 11 + internal/xcpu/cpu_other_arm.go | 9 + internal/xcpu/cpu_other_arm64.go | 9 + internal/xcpu/cpu_other_mips64x.go | 11 + internal/xcpu/cpu_other_ppc64x.go | 12 ++ internal/xcpu/cpu_other_riscv64.go | 11 + internal/xcpu/cpu_ppc64x.go | 16 ++ internal/xcpu/cpu_riscv64.go | 11 + internal/xcpu/cpu_s390x.go | 172 +++++++++++++++ internal/xcpu/cpu_s390x.s | 57 +++++ internal/xcpu/cpu_wasm.go | 17 ++ internal/xcpu/cpu_x86.go | 151 ++++++++++++++ internal/xcpu/cpu_x86.s | 26 +++ internal/xcpu/cpu_zos.go | 10 + internal/xcpu/cpu_zos_s390x.go | 25 +++ internal/xcpu/endian_big.go | 10 + internal/xcpu/endian_little.go | 10 + internal/xcpu/hwcap_linux.go | 71 +++++++ internal/xcpu/parse.go | 43 ++++ internal/xcpu/proc_cpuinfo_linux.go | 53 +++++ internal/xcpu/runtime_auxv.go | 16 ++ internal/xcpu/runtime_auxv_go121.go | 18 ++ internal/xcpu/syscall_aix_gccgo.go | 26 +++ internal/xcpu/syscall_aix_ppc64_gc.go | 35 ++++ mask_asm.go | 4 +- mask_test.go | 46 ++-- 59 files changed, 2242 insertions(+), 33 deletions(-) create mode 100644 internal/xcpu/.gitattributes create mode 100644 internal/xcpu/.gitignore create mode 100644 internal/xcpu/README.md create mode 100644 internal/xcpu/asm_aix_ppc64.s create mode 100644 internal/xcpu/byteorder.go create mode 100644 internal/xcpu/cpu.go create mode 100644 internal/xcpu/cpu_aix.go create mode 100644 internal/xcpu/cpu_arm.go create mode 100644 internal/xcpu/cpu_arm64.go create mode 100644 internal/xcpu/cpu_arm64.s create mode 100644 internal/xcpu/cpu_gc_arm64.go create mode 100644 internal/xcpu/cpu_gc_s390x.go create mode 100644 internal/xcpu/cpu_gc_x86.go create mode 100644 internal/xcpu/cpu_gccgo_arm64.go create mode 100644 internal/xcpu/cpu_gccgo_s390x.go create mode 100644 internal/xcpu/cpu_gccgo_x86.c create mode 100644 internal/xcpu/cpu_gccgo_x86.go create mode 100644 internal/xcpu/cpu_linux.go create mode 100644 internal/xcpu/cpu_linux_arm.go create mode 100644 internal/xcpu/cpu_linux_arm64.go create mode 100644 internal/xcpu/cpu_linux_mips64x.go create mode 100644 internal/xcpu/cpu_linux_noinit.go create mode 100644 internal/xcpu/cpu_linux_ppc64x.go create mode 100644 internal/xcpu/cpu_linux_s390x.go create mode 100644 internal/xcpu/cpu_loong64.go create mode 100644 internal/xcpu/cpu_mips64x.go create mode 100644 internal/xcpu/cpu_mipsx.go create mode 100644 internal/xcpu/cpu_netbsd_arm64.go create mode 100644 internal/xcpu/cpu_openbsd_arm64.go create mode 100644 internal/xcpu/cpu_openbsd_arm64.s create mode 100644 internal/xcpu/cpu_other_arm.go create mode 100644 internal/xcpu/cpu_other_arm64.go create mode 100644 internal/xcpu/cpu_other_mips64x.go create mode 100644 internal/xcpu/cpu_other_ppc64x.go create mode 100644 internal/xcpu/cpu_other_riscv64.go create mode 100644 internal/xcpu/cpu_ppc64x.go create mode 100644 internal/xcpu/cpu_riscv64.go create mode 100644 internal/xcpu/cpu_s390x.go create mode 100644 internal/xcpu/cpu_s390x.s create mode 100644 internal/xcpu/cpu_wasm.go create mode 100644 internal/xcpu/cpu_x86.go create mode 100644 internal/xcpu/cpu_x86.s create mode 100644 internal/xcpu/cpu_zos.go create mode 100644 internal/xcpu/cpu_zos_s390x.go create mode 100644 internal/xcpu/endian_big.go create mode 100644 internal/xcpu/endian_little.go create mode 100644 internal/xcpu/hwcap_linux.go create mode 100644 internal/xcpu/parse.go create mode 100644 internal/xcpu/proc_cpuinfo_linux.go create mode 100644 internal/xcpu/runtime_auxv.go create mode 100644 internal/xcpu/runtime_auxv_go121.go create mode 100644 internal/xcpu/syscall_aix_gccgo.go create mode 100644 internal/xcpu/syscall_aix_ppc64_gc.go diff --git a/go.mod b/go.mod index c6ec72cc..715a9f7a 100644 --- a/go.mod +++ b/go.mod @@ -1,5 +1,3 @@ module nhooyr.io/websocket go 1.19 - -require golang.org/x/sys v0.17.0 diff --git a/go.sum b/go.sum index 735d9a79..e69de29b 100644 --- a/go.sum +++ b/go.sum @@ -1,2 +0,0 @@ -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= diff --git a/internal/examples/go.mod b/internal/examples/go.mod index 50695945..c98b81ce 100644 --- a/internal/examples/go.mod +++ b/internal/examples/go.mod @@ -8,5 +8,3 @@ require ( golang.org/x/time v0.3.0 nhooyr.io/websocket v0.0.0-00010101000000-000000000000 ) - -require golang.org/x/sys v0.17.0 // indirect diff --git a/internal/examples/go.sum b/internal/examples/go.sum index 06068548..f8a07e82 100644 --- a/internal/examples/go.sum +++ b/internal/examples/go.sum @@ -1,4 +1,2 @@ -golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= diff --git a/internal/xcpu/.gitattributes b/internal/xcpu/.gitattributes new file mode 100644 index 00000000..d2f212e5 --- /dev/null +++ b/internal/xcpu/.gitattributes @@ -0,0 +1,10 @@ +# Treat all files in this repo as binary, with no git magic updating +# line endings. Windows users contributing to Go will need to use a +# modern version of git and editors capable of LF line endings. +# +# We'll prevent accidental CRLF line endings from entering the repo +# via the git-review gofmt checks. +# +# See golang.org/issue/9281 + +* -text diff --git a/internal/xcpu/.gitignore b/internal/xcpu/.gitignore new file mode 100644 index 00000000..5a9d62ef --- /dev/null +++ b/internal/xcpu/.gitignore @@ -0,0 +1,2 @@ +# Add no patterns to .gitignore except for files generated by the build. +last-change diff --git a/internal/xcpu/README.md b/internal/xcpu/README.md new file mode 100644 index 00000000..96a1a30f --- /dev/null +++ b/internal/xcpu/README.md @@ -0,0 +1,3 @@ +# cpu + +Vendored from https://github.com/golang/sys diff --git a/internal/xcpu/asm_aix_ppc64.s b/internal/xcpu/asm_aix_ppc64.s new file mode 100644 index 00000000..269e173c --- /dev/null +++ b/internal/xcpu/asm_aix_ppc64.s @@ -0,0 +1,17 @@ +// Copyright 2018 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. + +//go:build gc + +#include "textflag.h" + +// +// System calls for ppc64, AIX are implemented in runtime/syscall_aix.go +// + +TEXT ·syscall6(SB),NOSPLIT,$0-88 + JMP syscall·syscall6(SB) + +TEXT ·rawSyscall6(SB),NOSPLIT,$0-88 + JMP syscall·rawSyscall6(SB) diff --git a/internal/xcpu/byteorder.go b/internal/xcpu/byteorder.go new file mode 100644 index 00000000..8f28d86c --- /dev/null +++ b/internal/xcpu/byteorder.go @@ -0,0 +1,66 @@ +// Copyright 2019 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 xcpu + +import ( + "runtime" +) + +// byteOrder is a subset of encoding/binary.ByteOrder. +type byteOrder interface { + Uint32([]byte) uint32 + Uint64([]byte) uint64 +} + +type littleEndian struct{} +type bigEndian struct{} + +func (littleEndian) Uint32(b []byte) uint32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 +} + +func (littleEndian) Uint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | + uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 +} + +func (bigEndian) Uint32(b []byte) uint32 { + _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 + return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 +} + +func (bigEndian) Uint64(b []byte) uint64 { + _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 + return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | + uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 +} + +// hostByteOrder returns littleEndian on little-endian machines and +// bigEndian on big-endian machines. +func hostByteOrder() byteOrder { + switch runtime.GOARCH { + case "386", "amd64", "amd64p32", + "alpha", + "arm", "arm64", + "loong64", + "mipsle", "mips64le", "mips64p32le", + "nios2", + "ppc64le", + "riscv", "riscv64", + "sh": + return littleEndian{} + case "armbe", "arm64be", + "m68k", + "mips", "mips64", "mips64p32", + "ppc", "ppc64", + "s390", "s390x", + "shbe", + "sparc", "sparc64": + return bigEndian{} + } + panic("unknown architecture") +} diff --git a/internal/xcpu/cpu.go b/internal/xcpu/cpu.go new file mode 100644 index 00000000..5fc15019 --- /dev/null +++ b/internal/xcpu/cpu.go @@ -0,0 +1,290 @@ +// Copyright 2018 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 cpu implements processor feature detection for +// various CPU architectures. +package xcpu + +import ( + "os" + "strings" +) + +// Initialized reports whether the CPU features were initialized. +// +// For some GOOS/GOARCH combinations initialization of the CPU features depends +// on reading an operating specific file, e.g. /proc/self/auxv on linux/arm +// Initialized will report false if reading the file fails. +var Initialized bool + +// CacheLinePad is used to pad structs to avoid false sharing. +type CacheLinePad struct{ _ [cacheLineSize]byte } + +// X86 contains the supported CPU features of the +// current X86/AMD64 platform. If the current platform +// is not X86/AMD64 then all feature flags are false. +// +// X86 is padded to avoid false sharing. Further the HasAVX +// and HasAVX2 are only set if the OS supports XMM and YMM +// registers in addition to the CPUID feature bit being set. +var X86 struct { + _ CacheLinePad + HasAES bool // AES hardware implementation (AES NI) + HasADX bool // Multi-precision add-carry instruction extensions + HasAVX bool // Advanced vector extension + HasAVX2 bool // Advanced vector extension 2 + HasAVX512 bool // Advanced vector extension 512 + HasAVX512F bool // Advanced vector extension 512 Foundation Instructions + HasAVX512CD bool // Advanced vector extension 512 Conflict Detection Instructions + HasAVX512ER bool // Advanced vector extension 512 Exponential and Reciprocal Instructions + HasAVX512PF bool // Advanced vector extension 512 Prefetch Instructions + HasAVX512VL bool // Advanced vector extension 512 Vector Length Extensions + HasAVX512BW bool // Advanced vector extension 512 Byte and Word Instructions + HasAVX512DQ bool // Advanced vector extension 512 Doubleword and Quadword Instructions + HasAVX512IFMA bool // Advanced vector extension 512 Integer Fused Multiply Add + HasAVX512VBMI bool // Advanced vector extension 512 Vector Byte Manipulation Instructions + HasAVX5124VNNIW bool // Advanced vector extension 512 Vector Neural Network Instructions Word variable precision + HasAVX5124FMAPS bool // Advanced vector extension 512 Fused Multiply Accumulation Packed Single precision + HasAVX512VPOPCNTDQ bool // Advanced vector extension 512 Double and quad word population count instructions + HasAVX512VPCLMULQDQ bool // Advanced vector extension 512 Vector carry-less multiply operations + HasAVX512VNNI bool // Advanced vector extension 512 Vector Neural Network Instructions + HasAVX512GFNI bool // Advanced vector extension 512 Galois field New Instructions + HasAVX512VAES bool // Advanced vector extension 512 Vector AES instructions + HasAVX512VBMI2 bool // Advanced vector extension 512 Vector Byte Manipulation Instructions 2 + HasAVX512BITALG bool // Advanced vector extension 512 Bit Algorithms + HasAVX512BF16 bool // Advanced vector extension 512 BFloat16 Instructions + HasAMXTile bool // Advanced Matrix Extension Tile instructions + HasAMXInt8 bool // Advanced Matrix Extension Int8 instructions + HasAMXBF16 bool // Advanced Matrix Extension BFloat16 instructions + HasBMI1 bool // Bit manipulation instruction set 1 + HasBMI2 bool // Bit manipulation instruction set 2 + HasCX16 bool // Compare and exchange 16 Bytes + HasERMS bool // Enhanced REP for MOVSB and STOSB + HasFMA bool // Fused-multiply-add instructions + HasOSXSAVE bool // OS supports XSAVE/XRESTOR for saving/restoring XMM registers. + HasPCLMULQDQ bool // PCLMULQDQ instruction - most often used for AES-GCM + HasPOPCNT bool // Hamming weight instruction POPCNT. + HasRDRAND bool // RDRAND instruction (on-chip random number generator) + HasRDSEED bool // RDSEED instruction (on-chip random number generator) + HasSSE2 bool // Streaming SIMD extension 2 (always available on amd64) + HasSSE3 bool // Streaming SIMD extension 3 + HasSSSE3 bool // Supplemental streaming SIMD extension 3 + HasSSE41 bool // Streaming SIMD extension 4 and 4.1 + HasSSE42 bool // Streaming SIMD extension 4 and 4.2 + _ CacheLinePad +} + +// ARM64 contains the supported CPU features of the +// current ARMv8(aarch64) platform. If the current platform +// is not arm64 then all feature flags are false. +var ARM64 struct { + _ CacheLinePad + HasFP bool // Floating-point instruction set (always available) + HasASIMD bool // Advanced SIMD (always available) + HasEVTSTRM bool // Event stream support + HasAES bool // AES hardware implementation + HasPMULL bool // Polynomial multiplication instruction set + HasSHA1 bool // SHA1 hardware implementation + HasSHA2 bool // SHA2 hardware implementation + HasCRC32 bool // CRC32 hardware implementation + HasATOMICS bool // Atomic memory operation instruction set + HasFPHP bool // Half precision floating-point instruction set + HasASIMDHP bool // Advanced SIMD half precision instruction set + HasCPUID bool // CPUID identification scheme registers + HasASIMDRDM bool // Rounding double multiply add/subtract instruction set + HasJSCVT bool // Javascript conversion from floating-point to integer + HasFCMA bool // Floating-point multiplication and addition of complex numbers + HasLRCPC bool // Release Consistent processor consistent support + HasDCPOP bool // Persistent memory support + HasSHA3 bool // SHA3 hardware implementation + HasSM3 bool // SM3 hardware implementation + HasSM4 bool // SM4 hardware implementation + HasASIMDDP bool // Advanced SIMD double precision instruction set + HasSHA512 bool // SHA512 hardware implementation + HasSVE bool // Scalable Vector Extensions + HasASIMDFHM bool // Advanced SIMD multiplication FP16 to FP32 + _ CacheLinePad +} + +// ARM contains the supported CPU features of the current ARM (32-bit) platform. +// All feature flags are false if: +// 1. the current platform is not arm, or +// 2. the current operating system is not Linux. +var ARM struct { + _ CacheLinePad + HasSWP bool // SWP instruction support + HasHALF bool // Half-word load and store support + HasTHUMB bool // ARM Thumb instruction set + Has26BIT bool // Address space limited to 26-bits + HasFASTMUL bool // 32-bit operand, 64-bit result multiplication support + HasFPA bool // Floating point arithmetic support + HasVFP bool // Vector floating point support + HasEDSP bool // DSP Extensions support + HasJAVA bool // Java instruction set + HasIWMMXT bool // Intel Wireless MMX technology support + HasCRUNCH bool // MaverickCrunch context switching and handling + HasTHUMBEE bool // Thumb EE instruction set + HasNEON bool // NEON instruction set + HasVFPv3 bool // Vector floating point version 3 support + HasVFPv3D16 bool // Vector floating point version 3 D8-D15 + HasTLS bool // Thread local storage support + HasVFPv4 bool // Vector floating point version 4 support + HasIDIVA bool // Integer divide instruction support in ARM mode + HasIDIVT bool // Integer divide instruction support in Thumb mode + HasVFPD32 bool // Vector floating point version 3 D15-D31 + HasLPAE bool // Large Physical Address Extensions + HasEVTSTRM bool // Event stream support + HasAES bool // AES hardware implementation + HasPMULL bool // Polynomial multiplication instruction set + HasSHA1 bool // SHA1 hardware implementation + HasSHA2 bool // SHA2 hardware implementation + HasCRC32 bool // CRC32 hardware implementation + _ CacheLinePad +} + +// MIPS64X contains the supported CPU features of the current mips64/mips64le +// platforms. If the current platform is not mips64/mips64le or the current +// operating system is not Linux then all feature flags are false. +var MIPS64X struct { + _ CacheLinePad + HasMSA bool // MIPS SIMD architecture + _ CacheLinePad +} + +// PPC64 contains the supported CPU features of the current ppc64/ppc64le platforms. +// If the current platform is not ppc64/ppc64le then all feature flags are false. +// +// For ppc64/ppc64le, it is safe to check only for ISA level starting on ISA v3.00, +// since there are no optional categories. There are some exceptions that also +// require kernel support to work (DARN, SCV), so there are feature bits for +// those as well. The struct is padded to avoid false sharing. +var PPC64 struct { + _ CacheLinePad + HasDARN bool // Hardware random number generator (requires kernel enablement) + HasSCV bool // Syscall vectored (requires kernel enablement) + IsPOWER8 bool // ISA v2.07 (POWER8) + IsPOWER9 bool // ISA v3.00 (POWER9), implies IsPOWER8 + _ CacheLinePad +} + +// S390X contains the supported CPU features of the current IBM Z +// (s390x) platform. If the current platform is not IBM Z then all +// feature flags are false. +// +// S390X is padded to avoid false sharing. Further HasVX is only set +// if the OS supports vector registers in addition to the STFLE +// feature bit being set. +var S390X struct { + _ CacheLinePad + HasZARCH bool // z/Architecture mode is active [mandatory] + HasSTFLE bool // store facility list extended + HasLDISP bool // long (20-bit) displacements + HasEIMM bool // 32-bit immediates + HasDFP bool // decimal floating point + HasETF3EH bool // ETF-3 enhanced + HasMSA bool // message security assist (CPACF) + HasAES bool // KM-AES{128,192,256} functions + HasAESCBC bool // KMC-AES{128,192,256} functions + HasAESCTR bool // KMCTR-AES{128,192,256} functions + HasAESGCM bool // KMA-GCM-AES{128,192,256} functions + HasGHASH bool // KIMD-GHASH function + HasSHA1 bool // K{I,L}MD-SHA-1 functions + HasSHA256 bool // K{I,L}MD-SHA-256 functions + HasSHA512 bool // K{I,L}MD-SHA-512 functions + HasSHA3 bool // K{I,L}MD-SHA3-{224,256,384,512} and K{I,L}MD-SHAKE-{128,256} functions + HasVX bool // vector facility + HasVXE bool // vector-enhancements facility 1 + _ CacheLinePad +} + +func init() { + archInit() + initOptions() + processOptions() +} + +// options contains the cpu debug options that can be used in GODEBUG. +// Options are arch dependent and are added by the arch specific initOptions functions. +// Features that are mandatory for the specific GOARCH should have the Required field set +// (e.g. SSE2 on amd64). +var options []option + +// Option names should be lower case. e.g. avx instead of AVX. +type option struct { + Name string + Feature *bool + Specified bool // whether feature value was specified in GODEBUG + Enable bool // whether feature should be enabled + Required bool // whether feature is mandatory and can not be disabled +} + +func processOptions() { + env := os.Getenv("GODEBUG") +field: + for env != "" { + field := "" + i := strings.IndexByte(env, ',') + if i < 0 { + field, env = env, "" + } else { + field, env = env[:i], env[i+1:] + } + if len(field) < 4 || field[:4] != "cpu." { + continue + } + i = strings.IndexByte(field, '=') + if i < 0 { + print("GODEBUG sys/cpu: no value specified for \"", field, "\"\n") + continue + } + key, value := field[4:i], field[i+1:] // e.g. "SSE2", "on" + + var enable bool + switch value { + case "on": + enable = true + case "off": + enable = false + default: + print("GODEBUG sys/cpu: value \"", value, "\" not supported for cpu option \"", key, "\"\n") + continue field + } + + if key == "all" { + for i := range options { + options[i].Specified = true + options[i].Enable = enable || options[i].Required + } + continue field + } + + for i := range options { + if options[i].Name == key { + options[i].Specified = true + options[i].Enable = enable + continue field + } + } + + print("GODEBUG sys/cpu: unknown cpu feature \"", key, "\"\n") + } + + for _, o := range options { + if !o.Specified { + continue + } + + if o.Enable && !*o.Feature { + print("GODEBUG sys/cpu: can not enable \"", o.Name, "\", missing CPU support\n") + continue + } + + if !o.Enable && o.Required { + print("GODEBUG sys/cpu: can not disable \"", o.Name, "\", required CPU feature\n") + continue + } + + *o.Feature = o.Enable + } +} diff --git a/internal/xcpu/cpu_aix.go b/internal/xcpu/cpu_aix.go new file mode 100644 index 00000000..5e6e2583 --- /dev/null +++ b/internal/xcpu/cpu_aix.go @@ -0,0 +1,33 @@ +// Copyright 2019 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. + +//go:build aix + +package xcpu + +const ( + // getsystemcfg constants + _SC_IMPL = 2 + _IMPL_POWER8 = 0x10000 + _IMPL_POWER9 = 0x20000 +) + +func archInit() { + impl := getsystemcfg(_SC_IMPL) + if impl&_IMPL_POWER8 != 0 { + PPC64.IsPOWER8 = true + } + if impl&_IMPL_POWER9 != 0 { + PPC64.IsPOWER8 = true + PPC64.IsPOWER9 = true + } + + Initialized = true +} + +func getsystemcfg(label int) (n uint64) { + r0, _ := callgetsystemcfg(label) + n = uint64(r0) + return +} diff --git a/internal/xcpu/cpu_arm.go b/internal/xcpu/cpu_arm.go new file mode 100644 index 00000000..ff120458 --- /dev/null +++ b/internal/xcpu/cpu_arm.go @@ -0,0 +1,73 @@ +// Copyright 2018 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 xcpu + +const cacheLineSize = 32 + +// HWCAP/HWCAP2 bits. +// These are specific to Linux. +const ( + hwcap_SWP = 1 << 0 + hwcap_HALF = 1 << 1 + hwcap_THUMB = 1 << 2 + hwcap_26BIT = 1 << 3 + hwcap_FAST_MULT = 1 << 4 + hwcap_FPA = 1 << 5 + hwcap_VFP = 1 << 6 + hwcap_EDSP = 1 << 7 + hwcap_JAVA = 1 << 8 + hwcap_IWMMXT = 1 << 9 + hwcap_CRUNCH = 1 << 10 + hwcap_THUMBEE = 1 << 11 + hwcap_NEON = 1 << 12 + hwcap_VFPv3 = 1 << 13 + hwcap_VFPv3D16 = 1 << 14 + hwcap_TLS = 1 << 15 + hwcap_VFPv4 = 1 << 16 + hwcap_IDIVA = 1 << 17 + hwcap_IDIVT = 1 << 18 + hwcap_VFPD32 = 1 << 19 + hwcap_LPAE = 1 << 20 + hwcap_EVTSTRM = 1 << 21 + + hwcap2_AES = 1 << 0 + hwcap2_PMULL = 1 << 1 + hwcap2_SHA1 = 1 << 2 + hwcap2_SHA2 = 1 << 3 + hwcap2_CRC32 = 1 << 4 +) + +func initOptions() { + options = []option{ + {Name: "pmull", Feature: &ARM.HasPMULL}, + {Name: "sha1", Feature: &ARM.HasSHA1}, + {Name: "sha2", Feature: &ARM.HasSHA2}, + {Name: "swp", Feature: &ARM.HasSWP}, + {Name: "thumb", Feature: &ARM.HasTHUMB}, + {Name: "thumbee", Feature: &ARM.HasTHUMBEE}, + {Name: "tls", Feature: &ARM.HasTLS}, + {Name: "vfp", Feature: &ARM.HasVFP}, + {Name: "vfpd32", Feature: &ARM.HasVFPD32}, + {Name: "vfpv3", Feature: &ARM.HasVFPv3}, + {Name: "vfpv3d16", Feature: &ARM.HasVFPv3D16}, + {Name: "vfpv4", Feature: &ARM.HasVFPv4}, + {Name: "half", Feature: &ARM.HasHALF}, + {Name: "26bit", Feature: &ARM.Has26BIT}, + {Name: "fastmul", Feature: &ARM.HasFASTMUL}, + {Name: "fpa", Feature: &ARM.HasFPA}, + {Name: "edsp", Feature: &ARM.HasEDSP}, + {Name: "java", Feature: &ARM.HasJAVA}, + {Name: "iwmmxt", Feature: &ARM.HasIWMMXT}, + {Name: "crunch", Feature: &ARM.HasCRUNCH}, + {Name: "neon", Feature: &ARM.HasNEON}, + {Name: "idivt", Feature: &ARM.HasIDIVT}, + {Name: "idiva", Feature: &ARM.HasIDIVA}, + {Name: "lpae", Feature: &ARM.HasLPAE}, + {Name: "evtstrm", Feature: &ARM.HasEVTSTRM}, + {Name: "aes", Feature: &ARM.HasAES}, + {Name: "crc32", Feature: &ARM.HasCRC32}, + } + +} diff --git a/internal/xcpu/cpu_arm64.go b/internal/xcpu/cpu_arm64.go new file mode 100644 index 00000000..3d4113a5 --- /dev/null +++ b/internal/xcpu/cpu_arm64.go @@ -0,0 +1,172 @@ +// Copyright 2019 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 xcpu + +import "runtime" + +// cacheLineSize is used to prevent false sharing of cache lines. +// We choose 128 because Apple Silicon, a.k.a. M1, has 128-byte cache line size. +// It doesn't cost much and is much more future-proof. +const cacheLineSize = 128 + +func initOptions() { + options = []option{ + {Name: "fp", Feature: &ARM64.HasFP}, + {Name: "asimd", Feature: &ARM64.HasASIMD}, + {Name: "evstrm", Feature: &ARM64.HasEVTSTRM}, + {Name: "aes", Feature: &ARM64.HasAES}, + {Name: "fphp", Feature: &ARM64.HasFPHP}, + {Name: "jscvt", Feature: &ARM64.HasJSCVT}, + {Name: "lrcpc", Feature: &ARM64.HasLRCPC}, + {Name: "pmull", Feature: &ARM64.HasPMULL}, + {Name: "sha1", Feature: &ARM64.HasSHA1}, + {Name: "sha2", Feature: &ARM64.HasSHA2}, + {Name: "sha3", Feature: &ARM64.HasSHA3}, + {Name: "sha512", Feature: &ARM64.HasSHA512}, + {Name: "sm3", Feature: &ARM64.HasSM3}, + {Name: "sm4", Feature: &ARM64.HasSM4}, + {Name: "sve", Feature: &ARM64.HasSVE}, + {Name: "crc32", Feature: &ARM64.HasCRC32}, + {Name: "atomics", Feature: &ARM64.HasATOMICS}, + {Name: "asimdhp", Feature: &ARM64.HasASIMDHP}, + {Name: "cpuid", Feature: &ARM64.HasCPUID}, + {Name: "asimrdm", Feature: &ARM64.HasASIMDRDM}, + {Name: "fcma", Feature: &ARM64.HasFCMA}, + {Name: "dcpop", Feature: &ARM64.HasDCPOP}, + {Name: "asimddp", Feature: &ARM64.HasASIMDDP}, + {Name: "asimdfhm", Feature: &ARM64.HasASIMDFHM}, + } +} + +func archInit() { + switch runtime.GOOS { + case "freebsd": + readARM64Registers() + case "linux", "netbsd", "openbsd": + doinit() + default: + // Many platforms don't seem to allow reading these registers. + setMinimalFeatures() + } +} + +// setMinimalFeatures fakes the minimal ARM64 features expected by +// TestARM64minimalFeatures. +func setMinimalFeatures() { + ARM64.HasASIMD = true + ARM64.HasFP = true +} + +func readARM64Registers() { + Initialized = true + + parseARM64SystemRegisters(getisar0(), getisar1(), getpfr0()) +} + +func parseARM64SystemRegisters(isar0, isar1, pfr0 uint64) { + // ID_AA64ISAR0_EL1 + switch extractBits(isar0, 4, 7) { + case 1: + ARM64.HasAES = true + case 2: + ARM64.HasAES = true + ARM64.HasPMULL = true + } + + switch extractBits(isar0, 8, 11) { + case 1: + ARM64.HasSHA1 = true + } + + switch extractBits(isar0, 12, 15) { + case 1: + ARM64.HasSHA2 = true + case 2: + ARM64.HasSHA2 = true + ARM64.HasSHA512 = true + } + + switch extractBits(isar0, 16, 19) { + case 1: + ARM64.HasCRC32 = true + } + + switch extractBits(isar0, 20, 23) { + case 2: + ARM64.HasATOMICS = true + } + + switch extractBits(isar0, 28, 31) { + case 1: + ARM64.HasASIMDRDM = true + } + + switch extractBits(isar0, 32, 35) { + case 1: + ARM64.HasSHA3 = true + } + + switch extractBits(isar0, 36, 39) { + case 1: + ARM64.HasSM3 = true + } + + switch extractBits(isar0, 40, 43) { + case 1: + ARM64.HasSM4 = true + } + + switch extractBits(isar0, 44, 47) { + case 1: + ARM64.HasASIMDDP = true + } + + // ID_AA64ISAR1_EL1 + switch extractBits(isar1, 0, 3) { + case 1: + ARM64.HasDCPOP = true + } + + switch extractBits(isar1, 12, 15) { + case 1: + ARM64.HasJSCVT = true + } + + switch extractBits(isar1, 16, 19) { + case 1: + ARM64.HasFCMA = true + } + + switch extractBits(isar1, 20, 23) { + case 1: + ARM64.HasLRCPC = true + } + + // ID_AA64PFR0_EL1 + switch extractBits(pfr0, 16, 19) { + case 0: + ARM64.HasFP = true + case 1: + ARM64.HasFP = true + ARM64.HasFPHP = true + } + + switch extractBits(pfr0, 20, 23) { + case 0: + ARM64.HasASIMD = true + case 1: + ARM64.HasASIMD = true + ARM64.HasASIMDHP = true + } + + switch extractBits(pfr0, 32, 35) { + case 1: + ARM64.HasSVE = true + } +} + +func extractBits(data uint64, start, end uint) uint { + return (uint)(data>>start) & ((1 << (end - start + 1)) - 1) +} diff --git a/internal/xcpu/cpu_arm64.s b/internal/xcpu/cpu_arm64.s new file mode 100644 index 00000000..fcb9a388 --- /dev/null +++ b/internal/xcpu/cpu_arm64.s @@ -0,0 +1,31 @@ +// Copyright 2019 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. + +//go:build gc + +#include "textflag.h" + +// func getisar0() uint64 +TEXT ·getisar0(SB),NOSPLIT,$0-8 + // get Instruction Set Attributes 0 into x0 + // mrs x0, ID_AA64ISAR0_EL1 = d5380600 + WORD $0xd5380600 + MOVD R0, ret+0(FP) + RET + +// func getisar1() uint64 +TEXT ·getisar1(SB),NOSPLIT,$0-8 + // get Instruction Set Attributes 1 into x0 + // mrs x0, ID_AA64ISAR1_EL1 = d5380620 + WORD $0xd5380620 + MOVD R0, ret+0(FP) + RET + +// func getpfr0() uint64 +TEXT ·getpfr0(SB),NOSPLIT,$0-8 + // get Processor Feature Register 0 into x0 + // mrs x0, ID_AA64PFR0_EL1 = d5380400 + WORD $0xd5380400 + MOVD R0, ret+0(FP) + RET diff --git a/internal/xcpu/cpu_gc_arm64.go b/internal/xcpu/cpu_gc_arm64.go new file mode 100644 index 00000000..26d3050d --- /dev/null +++ b/internal/xcpu/cpu_gc_arm64.go @@ -0,0 +1,11 @@ +// Copyright 2019 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. + +//go:build gc + +package xcpu + +func getisar0() uint64 +func getisar1() uint64 +func getpfr0() uint64 diff --git a/internal/xcpu/cpu_gc_s390x.go b/internal/xcpu/cpu_gc_s390x.go new file mode 100644 index 00000000..34ca88b7 --- /dev/null +++ b/internal/xcpu/cpu_gc_s390x.go @@ -0,0 +1,21 @@ +// Copyright 2019 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. + +//go:build gc + +package xcpu + +// haveAsmFunctions reports whether the other functions in this file can +// be safely called. +func haveAsmFunctions() bool { return true } + +// The following feature detection functions are defined in cpu_s390x.s. +// They are likely to be expensive to call so the results should be cached. +func stfle() facilityList +func kmQuery() queryResult +func kmcQuery() queryResult +func kmctrQuery() queryResult +func kmaQuery() queryResult +func kimdQuery() queryResult +func klmdQuery() queryResult diff --git a/internal/xcpu/cpu_gc_x86.go b/internal/xcpu/cpu_gc_x86.go new file mode 100644 index 00000000..9d6f61c2 --- /dev/null +++ b/internal/xcpu/cpu_gc_x86.go @@ -0,0 +1,15 @@ +// Copyright 2018 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. + +//go:build (386 || amd64 || amd64p32) && gc + +package xcpu + +// cpuid is implemented in cpu_x86.s for gc compiler +// and in cpu_gccgo.c for gccgo. +func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) + +// xgetbv with ecx = 0 is implemented in cpu_x86.s for gc compiler +// and in cpu_gccgo.c for gccgo. +func xgetbv() (eax, edx uint32) diff --git a/internal/xcpu/cpu_gccgo_arm64.go b/internal/xcpu/cpu_gccgo_arm64.go new file mode 100644 index 00000000..d6c2a3a8 --- /dev/null +++ b/internal/xcpu/cpu_gccgo_arm64.go @@ -0,0 +1,11 @@ +// Copyright 2019 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. + +//go:build gccgo + +package xcpu + +func getisar0() uint64 { return 0 } +func getisar1() uint64 { return 0 } +func getpfr0() uint64 { return 0 } diff --git a/internal/xcpu/cpu_gccgo_s390x.go b/internal/xcpu/cpu_gccgo_s390x.go new file mode 100644 index 00000000..4deec625 --- /dev/null +++ b/internal/xcpu/cpu_gccgo_s390x.go @@ -0,0 +1,22 @@ +// Copyright 2019 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. + +//go:build gccgo + +package xcpu + +// haveAsmFunctions reports whether the other functions in this file can +// be safely called. +func haveAsmFunctions() bool { return false } + +// TODO(mundaym): the following feature detection functions are currently +// stubs. See https://golang.org/cl/162887 for how to fix this. +// They are likely to be expensive to call so the results should be cached. +func stfle() facilityList { panic("not implemented for gccgo") } +func kmQuery() queryResult { panic("not implemented for gccgo") } +func kmcQuery() queryResult { panic("not implemented for gccgo") } +func kmctrQuery() queryResult { panic("not implemented for gccgo") } +func kmaQuery() queryResult { panic("not implemented for gccgo") } +func kimdQuery() queryResult { panic("not implemented for gccgo") } +func klmdQuery() queryResult { panic("not implemented for gccgo") } diff --git a/internal/xcpu/cpu_gccgo_x86.c b/internal/xcpu/cpu_gccgo_x86.c new file mode 100644 index 00000000..3f73a05d --- /dev/null +++ b/internal/xcpu/cpu_gccgo_x86.c @@ -0,0 +1,37 @@ +// Copyright 2018 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. + +//go:build (386 || amd64 || amd64p32) && gccgo + +#include +#include +#include + +// Need to wrap __get_cpuid_count because it's declared as static. +int +gccgoGetCpuidCount(uint32_t leaf, uint32_t subleaf, + uint32_t *eax, uint32_t *ebx, + uint32_t *ecx, uint32_t *edx) +{ + return __get_cpuid_count(leaf, subleaf, eax, ebx, ecx, edx); +} + +#pragma GCC diagnostic ignored "-Wunknown-pragmas" +#pragma GCC push_options +#pragma GCC target("xsave") +#pragma clang attribute push (__attribute__((target("xsave"))), apply_to=function) + +// xgetbv reads the contents of an XCR (Extended Control Register) +// specified in the ECX register into registers EDX:EAX. +// Currently, the only supported value for XCR is 0. +void +gccgoXgetbv(uint32_t *eax, uint32_t *edx) +{ + uint64_t v = _xgetbv(0); + *eax = v & 0xffffffff; + *edx = v >> 32; +} + +#pragma clang attribute pop +#pragma GCC pop_options diff --git a/internal/xcpu/cpu_gccgo_x86.go b/internal/xcpu/cpu_gccgo_x86.go new file mode 100644 index 00000000..e66c6ee9 --- /dev/null +++ b/internal/xcpu/cpu_gccgo_x86.go @@ -0,0 +1,31 @@ +// Copyright 2018 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. + +//go:build (386 || amd64 || amd64p32) && gccgo + +package xcpu + +//extern gccgoGetCpuidCount +func gccgoGetCpuidCount(eaxArg, ecxArg uint32, eax, ebx, ecx, edx *uint32) + +func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) { + var a, b, c, d uint32 + gccgoGetCpuidCount(eaxArg, ecxArg, &a, &b, &c, &d) + return a, b, c, d +} + +//extern gccgoXgetbv +func gccgoXgetbv(eax, edx *uint32) + +func xgetbv() (eax, edx uint32) { + var a, d uint32 + gccgoXgetbv(&a, &d) + return a, d +} + +// gccgo doesn't build on Darwin, per: +// https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/gcc.rb#L76 +func darwinSupportsAVX512() bool { + return false +} diff --git a/internal/xcpu/cpu_linux.go b/internal/xcpu/cpu_linux.go new file mode 100644 index 00000000..10a48916 --- /dev/null +++ b/internal/xcpu/cpu_linux.go @@ -0,0 +1,15 @@ +// Copyright 2018 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. + +//go:build !386 && !amd64 && !amd64p32 && !arm64 + +package xcpu + +func archInit() { + if err := readHWCAP(); err != nil { + return + } + doinit() + Initialized = true +} diff --git a/internal/xcpu/cpu_linux_arm.go b/internal/xcpu/cpu_linux_arm.go new file mode 100644 index 00000000..28e32637 --- /dev/null +++ b/internal/xcpu/cpu_linux_arm.go @@ -0,0 +1,39 @@ +// Copyright 2019 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 xcpu + +func doinit() { + ARM.HasSWP = isSet(hwCap, hwcap_SWP) + ARM.HasHALF = isSet(hwCap, hwcap_HALF) + ARM.HasTHUMB = isSet(hwCap, hwcap_THUMB) + ARM.Has26BIT = isSet(hwCap, hwcap_26BIT) + ARM.HasFASTMUL = isSet(hwCap, hwcap_FAST_MULT) + ARM.HasFPA = isSet(hwCap, hwcap_FPA) + ARM.HasVFP = isSet(hwCap, hwcap_VFP) + ARM.HasEDSP = isSet(hwCap, hwcap_EDSP) + ARM.HasJAVA = isSet(hwCap, hwcap_JAVA) + ARM.HasIWMMXT = isSet(hwCap, hwcap_IWMMXT) + ARM.HasCRUNCH = isSet(hwCap, hwcap_CRUNCH) + ARM.HasTHUMBEE = isSet(hwCap, hwcap_THUMBEE) + ARM.HasNEON = isSet(hwCap, hwcap_NEON) + ARM.HasVFPv3 = isSet(hwCap, hwcap_VFPv3) + ARM.HasVFPv3D16 = isSet(hwCap, hwcap_VFPv3D16) + ARM.HasTLS = isSet(hwCap, hwcap_TLS) + ARM.HasVFPv4 = isSet(hwCap, hwcap_VFPv4) + ARM.HasIDIVA = isSet(hwCap, hwcap_IDIVA) + ARM.HasIDIVT = isSet(hwCap, hwcap_IDIVT) + ARM.HasVFPD32 = isSet(hwCap, hwcap_VFPD32) + ARM.HasLPAE = isSet(hwCap, hwcap_LPAE) + ARM.HasEVTSTRM = isSet(hwCap, hwcap_EVTSTRM) + ARM.HasAES = isSet(hwCap2, hwcap2_AES) + ARM.HasPMULL = isSet(hwCap2, hwcap2_PMULL) + ARM.HasSHA1 = isSet(hwCap2, hwcap2_SHA1) + ARM.HasSHA2 = isSet(hwCap2, hwcap2_SHA2) + ARM.HasCRC32 = isSet(hwCap2, hwcap2_CRC32) +} + +func isSet(hwc uint, value uint) bool { + return hwc&value != 0 +} diff --git a/internal/xcpu/cpu_linux_arm64.go b/internal/xcpu/cpu_linux_arm64.go new file mode 100644 index 00000000..481f450b --- /dev/null +++ b/internal/xcpu/cpu_linux_arm64.go @@ -0,0 +1,111 @@ +// Copyright 2018 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 xcpu + +import ( + "strings" + "syscall" +) + +// HWCAP/HWCAP2 bits. These are exposed by Linux. +const ( + hwcap_FP = 1 << 0 + hwcap_ASIMD = 1 << 1 + hwcap_EVTSTRM = 1 << 2 + hwcap_AES = 1 << 3 + hwcap_PMULL = 1 << 4 + hwcap_SHA1 = 1 << 5 + hwcap_SHA2 = 1 << 6 + hwcap_CRC32 = 1 << 7 + hwcap_ATOMICS = 1 << 8 + hwcap_FPHP = 1 << 9 + hwcap_ASIMDHP = 1 << 10 + hwcap_CPUID = 1 << 11 + hwcap_ASIMDRDM = 1 << 12 + hwcap_JSCVT = 1 << 13 + hwcap_FCMA = 1 << 14 + hwcap_LRCPC = 1 << 15 + hwcap_DCPOP = 1 << 16 + hwcap_SHA3 = 1 << 17 + hwcap_SM3 = 1 << 18 + hwcap_SM4 = 1 << 19 + hwcap_ASIMDDP = 1 << 20 + hwcap_SHA512 = 1 << 21 + hwcap_SVE = 1 << 22 + hwcap_ASIMDFHM = 1 << 23 +) + +// linuxKernelCanEmulateCPUID reports whether we're running +// on Linux 4.11+. Ideally we'd like to ask the question about +// whether the current kernel contains +// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=77c97b4ee21290f5f083173d957843b615abbff2 +// but the version number will have to do. +func linuxKernelCanEmulateCPUID() bool { + var un syscall.Utsname + syscall.Uname(&un) + var sb strings.Builder + for _, b := range un.Release[:] { + if b == 0 { + break + } + sb.WriteByte(byte(b)) + } + major, minor, _, ok := parseRelease(sb.String()) + return ok && (major > 4 || major == 4 && minor >= 11) +} + +func doinit() { + if err := readHWCAP(); err != nil { + // We failed to read /proc/self/auxv. This can happen if the binary has + // been given extra capabilities(7) with /bin/setcap. + // + // When this happens, we have two options. If the Linux kernel is new + // enough (4.11+), we can read the arm64 registers directly which'll + // trap into the kernel and then return back to userspace. + // + // But on older kernels, such as Linux 4.4.180 as used on many Synology + // devices, calling readARM64Registers (specifically getisar0) will + // cause a SIGILL and we'll die. So for older kernels, parse /proc/cpuinfo + // instead. + // + // See golang/go#57336. + if linuxKernelCanEmulateCPUID() { + readARM64Registers() + } else { + readLinuxProcCPUInfo() + } + return + } + + // HWCAP feature bits + ARM64.HasFP = isSet(hwCap, hwcap_FP) + ARM64.HasASIMD = isSet(hwCap, hwcap_ASIMD) + ARM64.HasEVTSTRM = isSet(hwCap, hwcap_EVTSTRM) + ARM64.HasAES = isSet(hwCap, hwcap_AES) + ARM64.HasPMULL = isSet(hwCap, hwcap_PMULL) + ARM64.HasSHA1 = isSet(hwCap, hwcap_SHA1) + ARM64.HasSHA2 = isSet(hwCap, hwcap_SHA2) + ARM64.HasCRC32 = isSet(hwCap, hwcap_CRC32) + ARM64.HasATOMICS = isSet(hwCap, hwcap_ATOMICS) + ARM64.HasFPHP = isSet(hwCap, hwcap_FPHP) + ARM64.HasASIMDHP = isSet(hwCap, hwcap_ASIMDHP) + ARM64.HasCPUID = isSet(hwCap, hwcap_CPUID) + ARM64.HasASIMDRDM = isSet(hwCap, hwcap_ASIMDRDM) + ARM64.HasJSCVT = isSet(hwCap, hwcap_JSCVT) + ARM64.HasFCMA = isSet(hwCap, hwcap_FCMA) + ARM64.HasLRCPC = isSet(hwCap, hwcap_LRCPC) + ARM64.HasDCPOP = isSet(hwCap, hwcap_DCPOP) + ARM64.HasSHA3 = isSet(hwCap, hwcap_SHA3) + ARM64.HasSM3 = isSet(hwCap, hwcap_SM3) + ARM64.HasSM4 = isSet(hwCap, hwcap_SM4) + ARM64.HasASIMDDP = isSet(hwCap, hwcap_ASIMDDP) + ARM64.HasSHA512 = isSet(hwCap, hwcap_SHA512) + ARM64.HasSVE = isSet(hwCap, hwcap_SVE) + ARM64.HasASIMDFHM = isSet(hwCap, hwcap_ASIMDFHM) +} + +func isSet(hwc uint, value uint) bool { + return hwc&value != 0 +} diff --git a/internal/xcpu/cpu_linux_mips64x.go b/internal/xcpu/cpu_linux_mips64x.go new file mode 100644 index 00000000..15fdee9c --- /dev/null +++ b/internal/xcpu/cpu_linux_mips64x.go @@ -0,0 +1,22 @@ +// 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. + +//go:build linux && (mips64 || mips64le) + +package xcpu + +// HWCAP bits. These are exposed by the Linux kernel 5.4. +const ( + // CPU features + hwcap_MIPS_MSA = 1 << 1 +) + +func doinit() { + // HWCAP feature bits + MIPS64X.HasMSA = isSet(hwCap, hwcap_MIPS_MSA) +} + +func isSet(hwc uint, value uint) bool { + return hwc&value != 0 +} diff --git a/internal/xcpu/cpu_linux_noinit.go b/internal/xcpu/cpu_linux_noinit.go new file mode 100644 index 00000000..878e56fb --- /dev/null +++ b/internal/xcpu/cpu_linux_noinit.go @@ -0,0 +1,9 @@ +// Copyright 2019 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. + +//go:build linux && !arm && !arm64 && !mips64 && !mips64le && !ppc64 && !ppc64le && !s390x + +package xcpu + +func doinit() {} diff --git a/internal/xcpu/cpu_linux_ppc64x.go b/internal/xcpu/cpu_linux_ppc64x.go new file mode 100644 index 00000000..6a8ea12a --- /dev/null +++ b/internal/xcpu/cpu_linux_ppc64x.go @@ -0,0 +1,30 @@ +// Copyright 2018 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. + +//go:build linux && (ppc64 || ppc64le) + +package xcpu + +// HWCAP/HWCAP2 bits. These are exposed by the kernel. +const ( + // ISA Level + _PPC_FEATURE2_ARCH_2_07 = 0x80000000 + _PPC_FEATURE2_ARCH_3_00 = 0x00800000 + + // CPU features + _PPC_FEATURE2_DARN = 0x00200000 + _PPC_FEATURE2_SCV = 0x00100000 +) + +func doinit() { + // HWCAP2 feature bits + PPC64.IsPOWER8 = isSet(hwCap2, _PPC_FEATURE2_ARCH_2_07) + PPC64.IsPOWER9 = isSet(hwCap2, _PPC_FEATURE2_ARCH_3_00) + PPC64.HasDARN = isSet(hwCap2, _PPC_FEATURE2_DARN) + PPC64.HasSCV = isSet(hwCap2, _PPC_FEATURE2_SCV) +} + +func isSet(hwc uint, value uint) bool { + return hwc&value != 0 +} diff --git a/internal/xcpu/cpu_linux_s390x.go b/internal/xcpu/cpu_linux_s390x.go new file mode 100644 index 00000000..ff0ca7f4 --- /dev/null +++ b/internal/xcpu/cpu_linux_s390x.go @@ -0,0 +1,40 @@ +// Copyright 2019 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 xcpu + +const ( + // bit mask values from /usr/include/bits/hwcap.h + hwcap_ZARCH = 2 + hwcap_STFLE = 4 + hwcap_MSA = 8 + hwcap_LDISP = 16 + hwcap_EIMM = 32 + hwcap_DFP = 64 + hwcap_ETF3EH = 256 + hwcap_VX = 2048 + hwcap_VXE = 8192 +) + +func initS390Xbase() { + // test HWCAP bit vector + has := func(featureMask uint) bool { + return hwCap&featureMask == featureMask + } + + // mandatory + S390X.HasZARCH = has(hwcap_ZARCH) + + // optional + S390X.HasSTFLE = has(hwcap_STFLE) + S390X.HasLDISP = has(hwcap_LDISP) + S390X.HasEIMM = has(hwcap_EIMM) + S390X.HasETF3EH = has(hwcap_ETF3EH) + S390X.HasDFP = has(hwcap_DFP) + S390X.HasMSA = has(hwcap_MSA) + S390X.HasVX = has(hwcap_VX) + if S390X.HasVX { + S390X.HasVXE = has(hwcap_VXE) + } +} diff --git a/internal/xcpu/cpu_loong64.go b/internal/xcpu/cpu_loong64.go new file mode 100644 index 00000000..fdb21c60 --- /dev/null +++ b/internal/xcpu/cpu_loong64.go @@ -0,0 +1,12 @@ +// Copyright 2022 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. + +//go:build loong64 + +package xcpu + +const cacheLineSize = 64 + +func initOptions() { +} diff --git a/internal/xcpu/cpu_mips64x.go b/internal/xcpu/cpu_mips64x.go new file mode 100644 index 00000000..447fee98 --- /dev/null +++ b/internal/xcpu/cpu_mips64x.go @@ -0,0 +1,15 @@ +// Copyright 2018 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. + +//go:build mips64 || mips64le + +package xcpu + +const cacheLineSize = 32 + +func initOptions() { + options = []option{ + {Name: "msa", Feature: &MIPS64X.HasMSA}, + } +} diff --git a/internal/xcpu/cpu_mipsx.go b/internal/xcpu/cpu_mipsx.go new file mode 100644 index 00000000..6efa1917 --- /dev/null +++ b/internal/xcpu/cpu_mipsx.go @@ -0,0 +1,11 @@ +// Copyright 2018 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. + +//go:build mips || mipsle + +package xcpu + +const cacheLineSize = 32 + +func initOptions() {} diff --git a/internal/xcpu/cpu_netbsd_arm64.go b/internal/xcpu/cpu_netbsd_arm64.go new file mode 100644 index 00000000..b84b4408 --- /dev/null +++ b/internal/xcpu/cpu_netbsd_arm64.go @@ -0,0 +1,173 @@ +// 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 xcpu + +import ( + "syscall" + "unsafe" +) + +// Minimal copy of functionality from x/sys/unix so the cpu package can call +// sysctl without depending on x/sys/unix. + +const ( + _CTL_QUERY = -2 + + _SYSCTL_VERS_1 = 0x1000000 +) + +var _zero uintptr + +func sysctl(mib []int32, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) { + var _p0 unsafe.Pointer + if len(mib) > 0 { + _p0 = unsafe.Pointer(&mib[0]) + } else { + _p0 = unsafe.Pointer(&_zero) + } + _, _, errno := syscall.Syscall6( + syscall.SYS___SYSCTL, + uintptr(_p0), + uintptr(len(mib)), + uintptr(unsafe.Pointer(old)), + uintptr(unsafe.Pointer(oldlen)), + uintptr(unsafe.Pointer(new)), + uintptr(newlen)) + if errno != 0 { + return errno + } + return nil +} + +type sysctlNode struct { + Flags uint32 + Num int32 + Name [32]int8 + Ver uint32 + __rsvd uint32 + Un [16]byte + _sysctl_size [8]byte + _sysctl_func [8]byte + _sysctl_parent [8]byte + _sysctl_desc [8]byte +} + +func sysctlNodes(mib []int32) ([]sysctlNode, error) { + var olen uintptr + + // Get a list of all sysctl nodes below the given MIB by performing + // a sysctl for the given MIB with CTL_QUERY appended. + mib = append(mib, _CTL_QUERY) + qnode := sysctlNode{Flags: _SYSCTL_VERS_1} + qp := (*byte)(unsafe.Pointer(&qnode)) + sz := unsafe.Sizeof(qnode) + if err := sysctl(mib, nil, &olen, qp, sz); err != nil { + return nil, err + } + + // Now that we know the size, get the actual nodes. + nodes := make([]sysctlNode, olen/sz) + np := (*byte)(unsafe.Pointer(&nodes[0])) + if err := sysctl(mib, np, &olen, qp, sz); err != nil { + return nil, err + } + + return nodes, nil +} + +func nametomib(name string) ([]int32, error) { + // Split name into components. + var parts []string + last := 0 + for i := 0; i < len(name); i++ { + if name[i] == '.' { + parts = append(parts, name[last:i]) + last = i + 1 + } + } + parts = append(parts, name[last:]) + + mib := []int32{} + // Discover the nodes and construct the MIB OID. + for partno, part := range parts { + nodes, err := sysctlNodes(mib) + if err != nil { + return nil, err + } + for _, node := range nodes { + n := make([]byte, 0) + for i := range node.Name { + if node.Name[i] != 0 { + n = append(n, byte(node.Name[i])) + } + } + if string(n) == part { + mib = append(mib, int32(node.Num)) + break + } + } + if len(mib) != partno+1 { + return nil, err + } + } + + return mib, nil +} + +// aarch64SysctlCPUID is struct aarch64_sysctl_cpu_id from NetBSD's +type aarch64SysctlCPUID struct { + midr uint64 /* Main ID Register */ + revidr uint64 /* Revision ID Register */ + mpidr uint64 /* Multiprocessor Affinity Register */ + aa64dfr0 uint64 /* A64 Debug Feature Register 0 */ + aa64dfr1 uint64 /* A64 Debug Feature Register 1 */ + aa64isar0 uint64 /* A64 Instruction Set Attribute Register 0 */ + aa64isar1 uint64 /* A64 Instruction Set Attribute Register 1 */ + aa64mmfr0 uint64 /* A64 Memory Model Feature Register 0 */ + aa64mmfr1 uint64 /* A64 Memory Model Feature Register 1 */ + aa64mmfr2 uint64 /* A64 Memory Model Feature Register 2 */ + aa64pfr0 uint64 /* A64 Processor Feature Register 0 */ + aa64pfr1 uint64 /* A64 Processor Feature Register 1 */ + aa64zfr0 uint64 /* A64 SVE Feature ID Register 0 */ + mvfr0 uint32 /* Media and VFP Feature Register 0 */ + mvfr1 uint32 /* Media and VFP Feature Register 1 */ + mvfr2 uint32 /* Media and VFP Feature Register 2 */ + pad uint32 + clidr uint64 /* Cache Level ID Register */ + ctr uint64 /* Cache Type Register */ +} + +func sysctlCPUID(name string) (*aarch64SysctlCPUID, error) { + mib, err := nametomib(name) + if err != nil { + return nil, err + } + + out := aarch64SysctlCPUID{} + n := unsafe.Sizeof(out) + _, _, errno := syscall.Syscall6( + syscall.SYS___SYSCTL, + uintptr(unsafe.Pointer(&mib[0])), + uintptr(len(mib)), + uintptr(unsafe.Pointer(&out)), + uintptr(unsafe.Pointer(&n)), + uintptr(0), + uintptr(0)) + if errno != 0 { + return nil, errno + } + return &out, nil +} + +func doinit() { + cpuid, err := sysctlCPUID("machdep.cpu0.cpu_id") + if err != nil { + setMinimalFeatures() + return + } + parseARM64SystemRegisters(cpuid.aa64isar0, cpuid.aa64isar1, cpuid.aa64pfr0) + + Initialized = true +} diff --git a/internal/xcpu/cpu_openbsd_arm64.go b/internal/xcpu/cpu_openbsd_arm64.go new file mode 100644 index 00000000..2459a486 --- /dev/null +++ b/internal/xcpu/cpu_openbsd_arm64.go @@ -0,0 +1,65 @@ +// Copyright 2022 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 xcpu + +import ( + "syscall" + "unsafe" +) + +// Minimal copy of functionality from x/sys/unix so the cpu package can call +// sysctl without depending on x/sys/unix. + +const ( + // From OpenBSD's sys/sysctl.h. + _CTL_MACHDEP = 7 + + // From OpenBSD's machine/cpu.h. + _CPU_ID_AA64ISAR0 = 2 + _CPU_ID_AA64ISAR1 = 3 +) + +// Implemented in the runtime package (runtime/sys_openbsd3.go) +func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) + +//go:linkname syscall_syscall6 syscall.syscall6 + +func sysctl(mib []uint32, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) { + _, _, errno := syscall_syscall6(libc_sysctl_trampoline_addr, uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)), uintptr(unsafe.Pointer(old)), uintptr(unsafe.Pointer(oldlen)), uintptr(unsafe.Pointer(new)), uintptr(newlen)) + if errno != 0 { + return errno + } + return nil +} + +var libc_sysctl_trampoline_addr uintptr + +//go:cgo_import_dynamic libc_sysctl sysctl "libc.so" + +func sysctlUint64(mib []uint32) (uint64, bool) { + var out uint64 + nout := unsafe.Sizeof(out) + if err := sysctl(mib, (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); err != nil { + return 0, false + } + return out, true +} + +func doinit() { + setMinimalFeatures() + + // Get ID_AA64ISAR0 and ID_AA64ISAR1 from sysctl. + isar0, ok := sysctlUint64([]uint32{_CTL_MACHDEP, _CPU_ID_AA64ISAR0}) + if !ok { + return + } + isar1, ok := sysctlUint64([]uint32{_CTL_MACHDEP, _CPU_ID_AA64ISAR1}) + if !ok { + return + } + parseARM64SystemRegisters(isar0, isar1, 0) + + Initialized = true +} diff --git a/internal/xcpu/cpu_openbsd_arm64.s b/internal/xcpu/cpu_openbsd_arm64.s new file mode 100644 index 00000000..054ba05d --- /dev/null +++ b/internal/xcpu/cpu_openbsd_arm64.s @@ -0,0 +1,11 @@ +// Copyright 2022 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. + +#include "textflag.h" + +TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 + JMP libc_sysctl(SB) + +GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 +DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) diff --git a/internal/xcpu/cpu_other_arm.go b/internal/xcpu/cpu_other_arm.go new file mode 100644 index 00000000..e3247948 --- /dev/null +++ b/internal/xcpu/cpu_other_arm.go @@ -0,0 +1,9 @@ +// 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. + +//go:build !linux && arm + +package xcpu + +func archInit() {} diff --git a/internal/xcpu/cpu_other_arm64.go b/internal/xcpu/cpu_other_arm64.go new file mode 100644 index 00000000..5257a0b6 --- /dev/null +++ b/internal/xcpu/cpu_other_arm64.go @@ -0,0 +1,9 @@ +// Copyright 2019 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. + +//go:build !linux && !netbsd && !openbsd && arm64 + +package xcpu + +func doinit() {} diff --git a/internal/xcpu/cpu_other_mips64x.go b/internal/xcpu/cpu_other_mips64x.go new file mode 100644 index 00000000..b1ddc9d5 --- /dev/null +++ b/internal/xcpu/cpu_other_mips64x.go @@ -0,0 +1,11 @@ +// 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. + +//go:build !linux && (mips64 || mips64le) + +package xcpu + +func archInit() { + Initialized = true +} diff --git a/internal/xcpu/cpu_other_ppc64x.go b/internal/xcpu/cpu_other_ppc64x.go new file mode 100644 index 00000000..00a08baa --- /dev/null +++ b/internal/xcpu/cpu_other_ppc64x.go @@ -0,0 +1,12 @@ +// Copyright 2022 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. + +//go:build !aix && !linux && (ppc64 || ppc64le) + +package xcpu + +func archInit() { + PPC64.IsPOWER8 = true + Initialized = true +} diff --git a/internal/xcpu/cpu_other_riscv64.go b/internal/xcpu/cpu_other_riscv64.go new file mode 100644 index 00000000..7f8fd1fc --- /dev/null +++ b/internal/xcpu/cpu_other_riscv64.go @@ -0,0 +1,11 @@ +// Copyright 2022 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. + +//go:build !linux && riscv64 + +package xcpu + +func archInit() { + Initialized = true +} diff --git a/internal/xcpu/cpu_ppc64x.go b/internal/xcpu/cpu_ppc64x.go new file mode 100644 index 00000000..22afeec2 --- /dev/null +++ b/internal/xcpu/cpu_ppc64x.go @@ -0,0 +1,16 @@ +// 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. + +//go:build ppc64 || ppc64le + +package xcpu + +const cacheLineSize = 128 + +func initOptions() { + options = []option{ + {Name: "darn", Feature: &PPC64.HasDARN}, + {Name: "scv", Feature: &PPC64.HasSCV}, + } +} diff --git a/internal/xcpu/cpu_riscv64.go b/internal/xcpu/cpu_riscv64.go new file mode 100644 index 00000000..28e57b68 --- /dev/null +++ b/internal/xcpu/cpu_riscv64.go @@ -0,0 +1,11 @@ +// Copyright 2019 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. + +//go:build riscv64 + +package xcpu + +const cacheLineSize = 64 + +func initOptions() {} diff --git a/internal/xcpu/cpu_s390x.go b/internal/xcpu/cpu_s390x.go new file mode 100644 index 00000000..e85a8c5d --- /dev/null +++ b/internal/xcpu/cpu_s390x.go @@ -0,0 +1,172 @@ +// 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 xcpu + +const cacheLineSize = 256 + +func initOptions() { + options = []option{ + {Name: "zarch", Feature: &S390X.HasZARCH, Required: true}, + {Name: "stfle", Feature: &S390X.HasSTFLE, Required: true}, + {Name: "ldisp", Feature: &S390X.HasLDISP, Required: true}, + {Name: "eimm", Feature: &S390X.HasEIMM, Required: true}, + {Name: "dfp", Feature: &S390X.HasDFP}, + {Name: "etf3eh", Feature: &S390X.HasETF3EH}, + {Name: "msa", Feature: &S390X.HasMSA}, + {Name: "aes", Feature: &S390X.HasAES}, + {Name: "aescbc", Feature: &S390X.HasAESCBC}, + {Name: "aesctr", Feature: &S390X.HasAESCTR}, + {Name: "aesgcm", Feature: &S390X.HasAESGCM}, + {Name: "ghash", Feature: &S390X.HasGHASH}, + {Name: "sha1", Feature: &S390X.HasSHA1}, + {Name: "sha256", Feature: &S390X.HasSHA256}, + {Name: "sha3", Feature: &S390X.HasSHA3}, + {Name: "sha512", Feature: &S390X.HasSHA512}, + {Name: "vx", Feature: &S390X.HasVX}, + {Name: "vxe", Feature: &S390X.HasVXE}, + } +} + +// bitIsSet reports whether the bit at index is set. The bit index +// is in big endian order, so bit index 0 is the leftmost bit. +func bitIsSet(bits []uint64, index uint) bool { + return bits[index/64]&((1<<63)>>(index%64)) != 0 +} + +// facility is a bit index for the named facility. +type facility uint8 + +const ( + // mandatory facilities + zarch facility = 1 // z architecture mode is active + stflef facility = 7 // store-facility-list-extended + ldisp facility = 18 // long-displacement + eimm facility = 21 // extended-immediate + + // miscellaneous facilities + dfp facility = 42 // decimal-floating-point + etf3eh facility = 30 // extended-translation 3 enhancement + + // cryptography facilities + msa facility = 17 // message-security-assist + msa3 facility = 76 // message-security-assist extension 3 + msa4 facility = 77 // message-security-assist extension 4 + msa5 facility = 57 // message-security-assist extension 5 + msa8 facility = 146 // message-security-assist extension 8 + msa9 facility = 155 // message-security-assist extension 9 + + // vector facilities + vx facility = 129 // vector facility + vxe facility = 135 // vector-enhancements 1 + vxe2 facility = 148 // vector-enhancements 2 +) + +// facilityList contains the result of an STFLE call. +// Bits are numbered in big endian order so the +// leftmost bit (the MSB) is at index 0. +type facilityList struct { + bits [4]uint64 +} + +// Has reports whether the given facilities are present. +func (s *facilityList) Has(fs ...facility) bool { + if len(fs) == 0 { + panic("no facility bits provided") + } + for _, f := range fs { + if !bitIsSet(s.bits[:], uint(f)) { + return false + } + } + return true +} + +// function is the code for the named cryptographic function. +type function uint8 + +const ( + // KM{,A,C,CTR} function codes + aes128 function = 18 // AES-128 + aes192 function = 19 // AES-192 + aes256 function = 20 // AES-256 + + // K{I,L}MD function codes + sha1 function = 1 // SHA-1 + sha256 function = 2 // SHA-256 + sha512 function = 3 // SHA-512 + sha3_224 function = 32 // SHA3-224 + sha3_256 function = 33 // SHA3-256 + sha3_384 function = 34 // SHA3-384 + sha3_512 function = 35 // SHA3-512 + shake128 function = 36 // SHAKE-128 + shake256 function = 37 // SHAKE-256 + + // KLMD function codes + ghash function = 65 // GHASH +) + +// queryResult contains the result of a Query function +// call. Bits are numbered in big endian order so the +// leftmost bit (the MSB) is at index 0. +type queryResult struct { + bits [2]uint64 +} + +// Has reports whether the given functions are present. +func (q *queryResult) Has(fns ...function) bool { + if len(fns) == 0 { + panic("no function codes provided") + } + for _, f := range fns { + if !bitIsSet(q.bits[:], uint(f)) { + return false + } + } + return true +} + +func doinit() { + initS390Xbase() + + // We need implementations of stfle, km and so on + // to detect cryptographic features. + if !haveAsmFunctions() { + return + } + + // optional cryptographic functions + if S390X.HasMSA { + aes := []function{aes128, aes192, aes256} + + // cipher message + km, kmc := kmQuery(), kmcQuery() + S390X.HasAES = km.Has(aes...) + S390X.HasAESCBC = kmc.Has(aes...) + if S390X.HasSTFLE { + facilities := stfle() + if facilities.Has(msa4) { + kmctr := kmctrQuery() + S390X.HasAESCTR = kmctr.Has(aes...) + } + if facilities.Has(msa8) { + kma := kmaQuery() + S390X.HasAESGCM = kma.Has(aes...) + } + } + + // compute message digest + kimd := kimdQuery() // intermediate (no padding) + klmd := klmdQuery() // last (padding) + S390X.HasSHA1 = kimd.Has(sha1) && klmd.Has(sha1) + S390X.HasSHA256 = kimd.Has(sha256) && klmd.Has(sha256) + S390X.HasSHA512 = kimd.Has(sha512) && klmd.Has(sha512) + S390X.HasGHASH = kimd.Has(ghash) // KLMD-GHASH does not exist + sha3 := []function{ + sha3_224, sha3_256, sha3_384, sha3_512, + shake128, shake256, + } + S390X.HasSHA3 = kimd.Has(sha3...) && klmd.Has(sha3...) + } +} diff --git a/internal/xcpu/cpu_s390x.s b/internal/xcpu/cpu_s390x.s new file mode 100644 index 00000000..1fb4b701 --- /dev/null +++ b/internal/xcpu/cpu_s390x.s @@ -0,0 +1,57 @@ +// Copyright 2019 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. + +//go:build gc + +#include "textflag.h" + +// func stfle() facilityList +TEXT ·stfle(SB), NOSPLIT|NOFRAME, $0-32 + MOVD $ret+0(FP), R1 + MOVD $3, R0 // last doubleword index to store + XC $32, (R1), (R1) // clear 4 doublewords (32 bytes) + WORD $0xb2b01000 // store facility list extended (STFLE) + RET + +// func kmQuery() queryResult +TEXT ·kmQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KM-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB92E0024 // cipher message (KM) + RET + +// func kmcQuery() queryResult +TEXT ·kmcQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KMC-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB92F0024 // cipher message with chaining (KMC) + RET + +// func kmctrQuery() queryResult +TEXT ·kmctrQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KMCTR-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB92D4024 // cipher message with counter (KMCTR) + RET + +// func kmaQuery() queryResult +TEXT ·kmaQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KMA-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xb9296024 // cipher message with authentication (KMA) + RET + +// func kimdQuery() queryResult +TEXT ·kimdQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KIMD-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB93E0024 // compute intermediate message digest (KIMD) + RET + +// func klmdQuery() queryResult +TEXT ·klmdQuery(SB), NOSPLIT|NOFRAME, $0-16 + MOVD $0, R0 // set function code to 0 (KLMD-Query) + MOVD $ret+0(FP), R1 // address of 16-byte return value + WORD $0xB93F0024 // compute last message digest (KLMD) + RET diff --git a/internal/xcpu/cpu_wasm.go b/internal/xcpu/cpu_wasm.go new file mode 100644 index 00000000..230aaab4 --- /dev/null +++ b/internal/xcpu/cpu_wasm.go @@ -0,0 +1,17 @@ +// Copyright 2019 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. + +//go:build wasm + +package xcpu + +// We're compiling the cpu package for an unknown (software-abstracted) CPU. +// Make CacheLinePad an empty struct and hope that the usual struct alignment +// rules are good enough. + +const cacheLineSize = 0 + +func initOptions() {} + +func archInit() {} diff --git a/internal/xcpu/cpu_x86.go b/internal/xcpu/cpu_x86.go new file mode 100644 index 00000000..d2f83468 --- /dev/null +++ b/internal/xcpu/cpu_x86.go @@ -0,0 +1,151 @@ +// Copyright 2018 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. + +//go:build 386 || amd64 || amd64p32 + +package xcpu + +import "runtime" + +const cacheLineSize = 64 + +func initOptions() { + options = []option{ + {Name: "adx", Feature: &X86.HasADX}, + {Name: "aes", Feature: &X86.HasAES}, + {Name: "avx", Feature: &X86.HasAVX}, + {Name: "avx2", Feature: &X86.HasAVX2}, + {Name: "avx512", Feature: &X86.HasAVX512}, + {Name: "avx512f", Feature: &X86.HasAVX512F}, + {Name: "avx512cd", Feature: &X86.HasAVX512CD}, + {Name: "avx512er", Feature: &X86.HasAVX512ER}, + {Name: "avx512pf", Feature: &X86.HasAVX512PF}, + {Name: "avx512vl", Feature: &X86.HasAVX512VL}, + {Name: "avx512bw", Feature: &X86.HasAVX512BW}, + {Name: "avx512dq", Feature: &X86.HasAVX512DQ}, + {Name: "avx512ifma", Feature: &X86.HasAVX512IFMA}, + {Name: "avx512vbmi", Feature: &X86.HasAVX512VBMI}, + {Name: "avx512vnniw", Feature: &X86.HasAVX5124VNNIW}, + {Name: "avx5124fmaps", Feature: &X86.HasAVX5124FMAPS}, + {Name: "avx512vpopcntdq", Feature: &X86.HasAVX512VPOPCNTDQ}, + {Name: "avx512vpclmulqdq", Feature: &X86.HasAVX512VPCLMULQDQ}, + {Name: "avx512vnni", Feature: &X86.HasAVX512VNNI}, + {Name: "avx512gfni", Feature: &X86.HasAVX512GFNI}, + {Name: "avx512vaes", Feature: &X86.HasAVX512VAES}, + {Name: "avx512vbmi2", Feature: &X86.HasAVX512VBMI2}, + {Name: "avx512bitalg", Feature: &X86.HasAVX512BITALG}, + {Name: "avx512bf16", Feature: &X86.HasAVX512BF16}, + {Name: "amxtile", Feature: &X86.HasAMXTile}, + {Name: "amxint8", Feature: &X86.HasAMXInt8}, + {Name: "amxbf16", Feature: &X86.HasAMXBF16}, + {Name: "bmi1", Feature: &X86.HasBMI1}, + {Name: "bmi2", Feature: &X86.HasBMI2}, + {Name: "cx16", Feature: &X86.HasCX16}, + {Name: "erms", Feature: &X86.HasERMS}, + {Name: "fma", Feature: &X86.HasFMA}, + {Name: "osxsave", Feature: &X86.HasOSXSAVE}, + {Name: "pclmulqdq", Feature: &X86.HasPCLMULQDQ}, + {Name: "popcnt", Feature: &X86.HasPOPCNT}, + {Name: "rdrand", Feature: &X86.HasRDRAND}, + {Name: "rdseed", Feature: &X86.HasRDSEED}, + {Name: "sse3", Feature: &X86.HasSSE3}, + {Name: "sse41", Feature: &X86.HasSSE41}, + {Name: "sse42", Feature: &X86.HasSSE42}, + {Name: "ssse3", Feature: &X86.HasSSSE3}, + + // These capabilities should always be enabled on amd64: + {Name: "sse2", Feature: &X86.HasSSE2, Required: runtime.GOARCH == "amd64"}, + } +} + +func archInit() { + + Initialized = true + + maxID, _, _, _ := cpuid(0, 0) + + if maxID < 1 { + return + } + + _, _, ecx1, edx1 := cpuid(1, 0) + X86.HasSSE2 = isSet(26, edx1) + + X86.HasSSE3 = isSet(0, ecx1) + X86.HasPCLMULQDQ = isSet(1, ecx1) + X86.HasSSSE3 = isSet(9, ecx1) + X86.HasFMA = isSet(12, ecx1) + X86.HasCX16 = isSet(13, ecx1) + X86.HasSSE41 = isSet(19, ecx1) + X86.HasSSE42 = isSet(20, ecx1) + X86.HasPOPCNT = isSet(23, ecx1) + X86.HasAES = isSet(25, ecx1) + X86.HasOSXSAVE = isSet(27, ecx1) + X86.HasRDRAND = isSet(30, ecx1) + + var osSupportsAVX, osSupportsAVX512 bool + // For XGETBV, OSXSAVE bit is required and sufficient. + if X86.HasOSXSAVE { + eax, _ := xgetbv() + // Check if XMM and YMM registers have OS support. + osSupportsAVX = isSet(1, eax) && isSet(2, eax) + + if runtime.GOOS == "darwin" { + // Darwin doesn't save/restore AVX-512 mask registers correctly across signal handlers. + // Since users can't rely on mask register contents, let's not advertise AVX-512 support. + // See issue 49233. + osSupportsAVX512 = false + } else { + // Check if OPMASK and ZMM registers have OS support. + osSupportsAVX512 = osSupportsAVX && isSet(5, eax) && isSet(6, eax) && isSet(7, eax) + } + } + + X86.HasAVX = isSet(28, ecx1) && osSupportsAVX + + if maxID < 7 { + return + } + + _, ebx7, ecx7, edx7 := cpuid(7, 0) + X86.HasBMI1 = isSet(3, ebx7) + X86.HasAVX2 = isSet(5, ebx7) && osSupportsAVX + X86.HasBMI2 = isSet(8, ebx7) + X86.HasERMS = isSet(9, ebx7) + X86.HasRDSEED = isSet(18, ebx7) + X86.HasADX = isSet(19, ebx7) + + X86.HasAVX512 = isSet(16, ebx7) && osSupportsAVX512 // Because avx-512 foundation is the core required extension + if X86.HasAVX512 { + X86.HasAVX512F = true + X86.HasAVX512CD = isSet(28, ebx7) + X86.HasAVX512ER = isSet(27, ebx7) + X86.HasAVX512PF = isSet(26, ebx7) + X86.HasAVX512VL = isSet(31, ebx7) + X86.HasAVX512BW = isSet(30, ebx7) + X86.HasAVX512DQ = isSet(17, ebx7) + X86.HasAVX512IFMA = isSet(21, ebx7) + X86.HasAVX512VBMI = isSet(1, ecx7) + X86.HasAVX5124VNNIW = isSet(2, edx7) + X86.HasAVX5124FMAPS = isSet(3, edx7) + X86.HasAVX512VPOPCNTDQ = isSet(14, ecx7) + X86.HasAVX512VPCLMULQDQ = isSet(10, ecx7) + X86.HasAVX512VNNI = isSet(11, ecx7) + X86.HasAVX512GFNI = isSet(8, ecx7) + X86.HasAVX512VAES = isSet(9, ecx7) + X86.HasAVX512VBMI2 = isSet(6, ecx7) + X86.HasAVX512BITALG = isSet(12, ecx7) + + eax71, _, _, _ := cpuid(7, 1) + X86.HasAVX512BF16 = isSet(5, eax71) + } + + X86.HasAMXTile = isSet(24, edx7) + X86.HasAMXInt8 = isSet(25, edx7) + X86.HasAMXBF16 = isSet(22, edx7) +} + +func isSet(bitpos uint, value uint32) bool { + return value&(1<> 63)) +) + +// For those platforms don't have a 'cpuid' equivalent we use HWCAP/HWCAP2 +// These are initialized in cpu_$GOARCH.go +// and should not be changed after they are initialized. +var hwCap uint +var hwCap2 uint + +func readHWCAP() error { + // For Go 1.21+, get auxv from the Go runtime. + if a := getAuxv(); len(a) > 0 { + for len(a) >= 2 { + tag, val := a[0], uint(a[1]) + a = a[2:] + switch tag { + case _AT_HWCAP: + hwCap = val + case _AT_HWCAP2: + hwCap2 = val + } + } + return nil + } + + buf, err := os.ReadFile(procAuxv) + if err != nil { + // e.g. on android /proc/self/auxv is not accessible, so silently + // ignore the error and leave Initialized = false. On some + // architectures (e.g. arm64) doinit() implements a fallback + // readout and will set Initialized = true again. + return err + } + bo := hostByteOrder() + for len(buf) >= 2*(uintSize/8) { + var tag, val uint + switch uintSize { + case 32: + tag = uint(bo.Uint32(buf[0:])) + val = uint(bo.Uint32(buf[4:])) + buf = buf[8:] + case 64: + tag = uint(bo.Uint64(buf[0:])) + val = uint(bo.Uint64(buf[8:])) + buf = buf[16:] + } + switch tag { + case _AT_HWCAP: + hwCap = val + case _AT_HWCAP2: + hwCap2 = val + } + } + return nil +} diff --git a/internal/xcpu/parse.go b/internal/xcpu/parse.go new file mode 100644 index 00000000..be30b60f --- /dev/null +++ b/internal/xcpu/parse.go @@ -0,0 +1,43 @@ +// Copyright 2022 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 xcpu + +import "strconv" + +// parseRelease parses a dot-separated version number. It follows the semver +// syntax, but allows the minor and patch versions to be elided. +// +// This is a copy of the Go runtime's parseRelease from +// https://golang.org/cl/209597. +func parseRelease(rel string) (major, minor, patch int, ok bool) { + // Strip anything after a dash or plus. + for i := 0; i < len(rel); i++ { + if rel[i] == '-' || rel[i] == '+' { + rel = rel[:i] + break + } + } + + next := func() (int, bool) { + for i := 0; i < len(rel); i++ { + if rel[i] == '.' { + ver, err := strconv.Atoi(rel[:i]) + rel = rel[i+1:] + return ver, err == nil + } + } + ver, err := strconv.Atoi(rel) + rel = "" + return ver, err == nil + } + if major, ok = next(); !ok || rel == "" { + return + } + if minor, ok = next(); !ok || rel == "" { + return + } + patch, ok = next() + return +} diff --git a/internal/xcpu/proc_cpuinfo_linux.go b/internal/xcpu/proc_cpuinfo_linux.go new file mode 100644 index 00000000..9c88d24e --- /dev/null +++ b/internal/xcpu/proc_cpuinfo_linux.go @@ -0,0 +1,53 @@ +// Copyright 2022 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. + +//go:build linux && arm64 + +package xcpu + +import ( + "errors" + "io" + "os" + "strings" +) + +func readLinuxProcCPUInfo() error { + f, err := os.Open("/proc/cpuinfo") + if err != nil { + return err + } + defer f.Close() + + var buf [1 << 10]byte // enough for first CPU + n, err := io.ReadFull(f, buf[:]) + if err != nil && err != io.ErrUnexpectedEOF { + return err + } + in := string(buf[:n]) + const features = "\nFeatures : " + i := strings.Index(in, features) + if i == -1 { + return errors.New("no CPU features found") + } + in = in[i+len(features):] + if i := strings.Index(in, "\n"); i != -1 { + in = in[:i] + } + m := map[string]*bool{} + + initOptions() // need it early here; it's harmless to call twice + for _, o := range options { + m[o.Name] = o.Feature + } + // The EVTSTRM field has alias "evstrm" in Go, but Linux calls it "evtstrm". + m["evtstrm"] = &ARM64.HasEVTSTRM + + for _, f := range strings.Fields(in) { + if p, ok := m[f]; ok { + *p = true + } + } + return nil +} diff --git a/internal/xcpu/runtime_auxv.go b/internal/xcpu/runtime_auxv.go new file mode 100644 index 00000000..b842842e --- /dev/null +++ b/internal/xcpu/runtime_auxv.go @@ -0,0 +1,16 @@ +// Copyright 2023 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 xcpu + +// getAuxvFn is non-nil on Go 1.21+ (via runtime_auxv_go121.go init) +// on platforms that use auxv. +var getAuxvFn func() []uintptr + +func getAuxv() []uintptr { + if getAuxvFn == nil { + return nil + } + return getAuxvFn() +} diff --git a/internal/xcpu/runtime_auxv_go121.go b/internal/xcpu/runtime_auxv_go121.go new file mode 100644 index 00000000..b4dba06a --- /dev/null +++ b/internal/xcpu/runtime_auxv_go121.go @@ -0,0 +1,18 @@ +// Copyright 2023 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. + +//go:build go1.21 + +package xcpu + +import ( + _ "unsafe" // for linkname +) + +//go:linkname runtime_getAuxv runtime.getAuxv +func runtime_getAuxv() []uintptr + +func init() { + getAuxvFn = runtime_getAuxv +} diff --git a/internal/xcpu/syscall_aix_gccgo.go b/internal/xcpu/syscall_aix_gccgo.go new file mode 100644 index 00000000..905566fe --- /dev/null +++ b/internal/xcpu/syscall_aix_gccgo.go @@ -0,0 +1,26 @@ +// 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. + +// Recreate a getsystemcfg syscall handler instead of +// using the one provided by x/sys/unix to avoid having +// the dependency between them. (See golang.org/issue/32102) +// Moreover, this file will be used during the building of +// gccgo's libgo and thus must not used a CGo method. + +//go:build aix && gccgo + +package xcpu + +import ( + "syscall" +) + +//extern getsystemcfg +func gccgoGetsystemcfg(label uint32) (r uint64) + +func callgetsystemcfg(label int) (r1 uintptr, e1 syscall.Errno) { + r1 = uintptr(gccgoGetsystemcfg(uint32(label))) + e1 = syscall.GetErrno() + return +} diff --git a/internal/xcpu/syscall_aix_ppc64_gc.go b/internal/xcpu/syscall_aix_ppc64_gc.go new file mode 100644 index 00000000..18837396 --- /dev/null +++ b/internal/xcpu/syscall_aix_ppc64_gc.go @@ -0,0 +1,35 @@ +// Copyright 2019 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. + +// Minimal copy of x/sys/unix so the cpu package can make a +// system call on AIX without depending on x/sys/unix. +// (See golang.org/issue/32102) + +//go:build aix && ppc64 && gc + +package xcpu + +import ( + "syscall" + "unsafe" +) + +//go:cgo_import_dynamic libc_getsystemcfg getsystemcfg "libc.a/shr_64.o" + +//go:linkname libc_getsystemcfg libc_getsystemcfg + +type syscallFunc uintptr + +var libc_getsystemcfg syscallFunc + +type errno = syscall.Errno + +// Implemented in runtime/syscall_aix.go. +func rawSyscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err errno) +func syscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err errno) + +func callgetsystemcfg(label int) (r1 uintptr, e1 errno) { + r1, _, e1 = syscall6(uintptr(unsafe.Pointer(&libc_getsystemcfg)), 1, uintptr(label), 0, 0, 0, 0, 0) + return +} diff --git a/mask_asm.go b/mask_asm.go index bf4bb635..3b1ee517 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,7 +2,7 @@ package websocket -import "golang.org/x/sys/cpu" +import "nhooyr.io/websocket/internal/xcpu" func mask(b []byte, key uint32) uint32 { if len(b) > 0 { @@ -12,7 +12,7 @@ func mask(b []byte, key uint32) uint32 { } //lint:ignore U1000 mask_*.s -var useAVX2 = cpu.X86.HasAVX2 +var useAVX2 = xcpu.X86.HasAVX2 // @nhooyr: I am not confident that the amd64 or the arm64 implementations of this // function are perfect. There are almost certainly missing optimizations or diff --git a/mask_test.go b/mask_test.go index 5c3d43c4..54f55e43 100644 --- a/mask_test.go +++ b/mask_test.go @@ -40,34 +40,34 @@ func TestMask(t *testing.T) { func testMask(t *testing.T, name string, fn func(b []byte, key uint32) uint32) { t.Run(name, func(t *testing.T) { t.Parallel() - for i := 0; i < 9999; i++ { - keyb := make([]byte, 4) - _, err := rand.Read(keyb) - assert.Success(t, err) - key := binary.LittleEndian.Uint32(keyb) + for i := 0; i < 9999; i++ { + keyb := make([]byte, 4) + _, err := rand.Read(keyb) + assert.Success(t, err) + key := binary.LittleEndian.Uint32(keyb) - n, err := rand.Int(rand.Reader, big.NewInt(1<<16)) - assert.Success(t, err) + n, err := rand.Int(rand.Reader, big.NewInt(1<<16)) + assert.Success(t, err) - b := make([]byte, 1+n.Int64()) - _, err = rand.Read(b) - assert.Success(t, err) + b := make([]byte, 1+n.Int64()) + _, err = rand.Read(b) + assert.Success(t, err) - b2 := make([]byte, len(b)) - copy(b2, b) - b3 := make([]byte, len(b)) - copy(b3, b) + b2 := make([]byte, len(b)) + copy(b2, b) + b3 := make([]byte, len(b)) + copy(b3, b) - key2 := basicMask(b2, key) - key3 := fn(b3, key) + key2 := basicMask(b2, key) + key3 := fn(b3, key) - if key2 != key3 { - t.Errorf("expected key %X but got %X", key2, key3) + if key2 != key3 { + t.Errorf("expected key %X but got %X", key2, key3) + } + if !bytes.Equal(b2, b3) { + t.Error("bad bytes") + return + } } - if !bytes.Equal(b2, b3) { - t.Error("bad bytes") - return - } - } }) } From 17e1b864a276ee7a45d6b14f4ed8445a05de543c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 05:14:52 -0800 Subject: [PATCH 138/152] mask_asm: Disable AVX2 See https://github.com/nhooyr/websocket/pull/326#issuecomment-1771138049 --- README.md | 2 +- internal/xcpu/.gitattributes | 10 - internal/xcpu/.gitignore | 2 - internal/xcpu/README.md | 3 - internal/xcpu/asm_aix_ppc64.s | 17 -- internal/xcpu/byteorder.go | 66 ------ internal/xcpu/cpu.go | 290 -------------------------- internal/xcpu/cpu_aix.go | 33 --- internal/xcpu/cpu_arm.go | 73 ------- internal/xcpu/cpu_arm64.go | 172 --------------- internal/xcpu/cpu_arm64.s | 31 --- internal/xcpu/cpu_gc_arm64.go | 11 - internal/xcpu/cpu_gc_s390x.go | 21 -- internal/xcpu/cpu_gc_x86.go | 15 -- internal/xcpu/cpu_gccgo_arm64.go | 11 - internal/xcpu/cpu_gccgo_s390x.go | 22 -- internal/xcpu/cpu_gccgo_x86.c | 37 ---- internal/xcpu/cpu_gccgo_x86.go | 31 --- internal/xcpu/cpu_linux.go | 15 -- internal/xcpu/cpu_linux_arm.go | 39 ---- internal/xcpu/cpu_linux_arm64.go | 111 ---------- internal/xcpu/cpu_linux_mips64x.go | 22 -- internal/xcpu/cpu_linux_noinit.go | 9 - internal/xcpu/cpu_linux_ppc64x.go | 30 --- internal/xcpu/cpu_linux_s390x.go | 40 ---- internal/xcpu/cpu_loong64.go | 12 -- internal/xcpu/cpu_mips64x.go | 15 -- internal/xcpu/cpu_mipsx.go | 11 - internal/xcpu/cpu_netbsd_arm64.go | 173 --------------- internal/xcpu/cpu_openbsd_arm64.go | 65 ------ internal/xcpu/cpu_openbsd_arm64.s | 11 - internal/xcpu/cpu_other_arm.go | 9 - internal/xcpu/cpu_other_arm64.go | 9 - internal/xcpu/cpu_other_mips64x.go | 11 - internal/xcpu/cpu_other_ppc64x.go | 12 -- internal/xcpu/cpu_other_riscv64.go | 11 - internal/xcpu/cpu_ppc64x.go | 16 -- internal/xcpu/cpu_riscv64.go | 11 - internal/xcpu/cpu_s390x.go | 172 --------------- internal/xcpu/cpu_s390x.s | 57 ----- internal/xcpu/cpu_wasm.go | 17 -- internal/xcpu/cpu_x86.go | 151 -------------- internal/xcpu/cpu_x86.s | 26 --- internal/xcpu/cpu_zos.go | 10 - internal/xcpu/cpu_zos_s390x.go | 25 --- internal/xcpu/endian_big.go | 10 - internal/xcpu/endian_little.go | 10 - internal/xcpu/hwcap_linux.go | 71 ------- internal/xcpu/parse.go | 43 ---- internal/xcpu/proc_cpuinfo_linux.go | 53 ----- internal/xcpu/runtime_auxv.go | 16 -- internal/xcpu/runtime_auxv_go121.go | 18 -- internal/xcpu/syscall_aix_gccgo.go | 26 --- internal/xcpu/syscall_aix_ppc64_gc.go | 35 ---- mask_amd64.s | 29 +-- mask_asm.go | 9 +- 56 files changed, 6 insertions(+), 2251 deletions(-) delete mode 100644 internal/xcpu/.gitattributes delete mode 100644 internal/xcpu/.gitignore delete mode 100644 internal/xcpu/README.md delete mode 100644 internal/xcpu/asm_aix_ppc64.s delete mode 100644 internal/xcpu/byteorder.go delete mode 100644 internal/xcpu/cpu.go delete mode 100644 internal/xcpu/cpu_aix.go delete mode 100644 internal/xcpu/cpu_arm.go delete mode 100644 internal/xcpu/cpu_arm64.go delete mode 100644 internal/xcpu/cpu_arm64.s delete mode 100644 internal/xcpu/cpu_gc_arm64.go delete mode 100644 internal/xcpu/cpu_gc_s390x.go delete mode 100644 internal/xcpu/cpu_gc_x86.go delete mode 100644 internal/xcpu/cpu_gccgo_arm64.go delete mode 100644 internal/xcpu/cpu_gccgo_s390x.go delete mode 100644 internal/xcpu/cpu_gccgo_x86.c delete mode 100644 internal/xcpu/cpu_gccgo_x86.go delete mode 100644 internal/xcpu/cpu_linux.go delete mode 100644 internal/xcpu/cpu_linux_arm.go delete mode 100644 internal/xcpu/cpu_linux_arm64.go delete mode 100644 internal/xcpu/cpu_linux_mips64x.go delete mode 100644 internal/xcpu/cpu_linux_noinit.go delete mode 100644 internal/xcpu/cpu_linux_ppc64x.go delete mode 100644 internal/xcpu/cpu_linux_s390x.go delete mode 100644 internal/xcpu/cpu_loong64.go delete mode 100644 internal/xcpu/cpu_mips64x.go delete mode 100644 internal/xcpu/cpu_mipsx.go delete mode 100644 internal/xcpu/cpu_netbsd_arm64.go delete mode 100644 internal/xcpu/cpu_openbsd_arm64.go delete mode 100644 internal/xcpu/cpu_openbsd_arm64.s delete mode 100644 internal/xcpu/cpu_other_arm.go delete mode 100644 internal/xcpu/cpu_other_arm64.go delete mode 100644 internal/xcpu/cpu_other_mips64x.go delete mode 100644 internal/xcpu/cpu_other_ppc64x.go delete mode 100644 internal/xcpu/cpu_other_riscv64.go delete mode 100644 internal/xcpu/cpu_ppc64x.go delete mode 100644 internal/xcpu/cpu_riscv64.go delete mode 100644 internal/xcpu/cpu_s390x.go delete mode 100644 internal/xcpu/cpu_s390x.s delete mode 100644 internal/xcpu/cpu_wasm.go delete mode 100644 internal/xcpu/cpu_x86.go delete mode 100644 internal/xcpu/cpu_x86.s delete mode 100644 internal/xcpu/cpu_zos.go delete mode 100644 internal/xcpu/cpu_zos_s390x.go delete mode 100644 internal/xcpu/endian_big.go delete mode 100644 internal/xcpu/endian_little.go delete mode 100644 internal/xcpu/hwcap_linux.go delete mode 100644 internal/xcpu/parse.go delete mode 100644 internal/xcpu/proc_cpuinfo_linux.go delete mode 100644 internal/xcpu/runtime_auxv.go delete mode 100644 internal/xcpu/runtime_auxv_go121.go delete mode 100644 internal/xcpu/syscall_aix_gccgo.go delete mode 100644 internal/xcpu/syscall_aix_ppc64_gc.go diff --git a/README.md b/README.md index 0f286e63..3dead855 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [4x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go +- [3-4x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode diff --git a/internal/xcpu/.gitattributes b/internal/xcpu/.gitattributes deleted file mode 100644 index d2f212e5..00000000 --- a/internal/xcpu/.gitattributes +++ /dev/null @@ -1,10 +0,0 @@ -# Treat all files in this repo as binary, with no git magic updating -# line endings. Windows users contributing to Go will need to use a -# modern version of git and editors capable of LF line endings. -# -# We'll prevent accidental CRLF line endings from entering the repo -# via the git-review gofmt checks. -# -# See golang.org/issue/9281 - -* -text diff --git a/internal/xcpu/.gitignore b/internal/xcpu/.gitignore deleted file mode 100644 index 5a9d62ef..00000000 --- a/internal/xcpu/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Add no patterns to .gitignore except for files generated by the build. -last-change diff --git a/internal/xcpu/README.md b/internal/xcpu/README.md deleted file mode 100644 index 96a1a30f..00000000 --- a/internal/xcpu/README.md +++ /dev/null @@ -1,3 +0,0 @@ -# cpu - -Vendored from https://github.com/golang/sys diff --git a/internal/xcpu/asm_aix_ppc64.s b/internal/xcpu/asm_aix_ppc64.s deleted file mode 100644 index 269e173c..00000000 --- a/internal/xcpu/asm_aix_ppc64.s +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2018 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. - -//go:build gc - -#include "textflag.h" - -// -// System calls for ppc64, AIX are implemented in runtime/syscall_aix.go -// - -TEXT ·syscall6(SB),NOSPLIT,$0-88 - JMP syscall·syscall6(SB) - -TEXT ·rawSyscall6(SB),NOSPLIT,$0-88 - JMP syscall·rawSyscall6(SB) diff --git a/internal/xcpu/byteorder.go b/internal/xcpu/byteorder.go deleted file mode 100644 index 8f28d86c..00000000 --- a/internal/xcpu/byteorder.go +++ /dev/null @@ -1,66 +0,0 @@ -// Copyright 2019 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 xcpu - -import ( - "runtime" -) - -// byteOrder is a subset of encoding/binary.ByteOrder. -type byteOrder interface { - Uint32([]byte) uint32 - Uint64([]byte) uint64 -} - -type littleEndian struct{} -type bigEndian struct{} - -func (littleEndian) Uint32(b []byte) uint32 { - _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 - return uint32(b[0]) | uint32(b[1])<<8 | uint32(b[2])<<16 | uint32(b[3])<<24 -} - -func (littleEndian) Uint64(b []byte) uint64 { - _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 - return uint64(b[0]) | uint64(b[1])<<8 | uint64(b[2])<<16 | uint64(b[3])<<24 | - uint64(b[4])<<32 | uint64(b[5])<<40 | uint64(b[6])<<48 | uint64(b[7])<<56 -} - -func (bigEndian) Uint32(b []byte) uint32 { - _ = b[3] // bounds check hint to compiler; see golang.org/issue/14808 - return uint32(b[3]) | uint32(b[2])<<8 | uint32(b[1])<<16 | uint32(b[0])<<24 -} - -func (bigEndian) Uint64(b []byte) uint64 { - _ = b[7] // bounds check hint to compiler; see golang.org/issue/14808 - return uint64(b[7]) | uint64(b[6])<<8 | uint64(b[5])<<16 | uint64(b[4])<<24 | - uint64(b[3])<<32 | uint64(b[2])<<40 | uint64(b[1])<<48 | uint64(b[0])<<56 -} - -// hostByteOrder returns littleEndian on little-endian machines and -// bigEndian on big-endian machines. -func hostByteOrder() byteOrder { - switch runtime.GOARCH { - case "386", "amd64", "amd64p32", - "alpha", - "arm", "arm64", - "loong64", - "mipsle", "mips64le", "mips64p32le", - "nios2", - "ppc64le", - "riscv", "riscv64", - "sh": - return littleEndian{} - case "armbe", "arm64be", - "m68k", - "mips", "mips64", "mips64p32", - "ppc", "ppc64", - "s390", "s390x", - "shbe", - "sparc", "sparc64": - return bigEndian{} - } - panic("unknown architecture") -} diff --git a/internal/xcpu/cpu.go b/internal/xcpu/cpu.go deleted file mode 100644 index 5fc15019..00000000 --- a/internal/xcpu/cpu.go +++ /dev/null @@ -1,290 +0,0 @@ -// Copyright 2018 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 cpu implements processor feature detection for -// various CPU architectures. -package xcpu - -import ( - "os" - "strings" -) - -// Initialized reports whether the CPU features were initialized. -// -// For some GOOS/GOARCH combinations initialization of the CPU features depends -// on reading an operating specific file, e.g. /proc/self/auxv on linux/arm -// Initialized will report false if reading the file fails. -var Initialized bool - -// CacheLinePad is used to pad structs to avoid false sharing. -type CacheLinePad struct{ _ [cacheLineSize]byte } - -// X86 contains the supported CPU features of the -// current X86/AMD64 platform. If the current platform -// is not X86/AMD64 then all feature flags are false. -// -// X86 is padded to avoid false sharing. Further the HasAVX -// and HasAVX2 are only set if the OS supports XMM and YMM -// registers in addition to the CPUID feature bit being set. -var X86 struct { - _ CacheLinePad - HasAES bool // AES hardware implementation (AES NI) - HasADX bool // Multi-precision add-carry instruction extensions - HasAVX bool // Advanced vector extension - HasAVX2 bool // Advanced vector extension 2 - HasAVX512 bool // Advanced vector extension 512 - HasAVX512F bool // Advanced vector extension 512 Foundation Instructions - HasAVX512CD bool // Advanced vector extension 512 Conflict Detection Instructions - HasAVX512ER bool // Advanced vector extension 512 Exponential and Reciprocal Instructions - HasAVX512PF bool // Advanced vector extension 512 Prefetch Instructions - HasAVX512VL bool // Advanced vector extension 512 Vector Length Extensions - HasAVX512BW bool // Advanced vector extension 512 Byte and Word Instructions - HasAVX512DQ bool // Advanced vector extension 512 Doubleword and Quadword Instructions - HasAVX512IFMA bool // Advanced vector extension 512 Integer Fused Multiply Add - HasAVX512VBMI bool // Advanced vector extension 512 Vector Byte Manipulation Instructions - HasAVX5124VNNIW bool // Advanced vector extension 512 Vector Neural Network Instructions Word variable precision - HasAVX5124FMAPS bool // Advanced vector extension 512 Fused Multiply Accumulation Packed Single precision - HasAVX512VPOPCNTDQ bool // Advanced vector extension 512 Double and quad word population count instructions - HasAVX512VPCLMULQDQ bool // Advanced vector extension 512 Vector carry-less multiply operations - HasAVX512VNNI bool // Advanced vector extension 512 Vector Neural Network Instructions - HasAVX512GFNI bool // Advanced vector extension 512 Galois field New Instructions - HasAVX512VAES bool // Advanced vector extension 512 Vector AES instructions - HasAVX512VBMI2 bool // Advanced vector extension 512 Vector Byte Manipulation Instructions 2 - HasAVX512BITALG bool // Advanced vector extension 512 Bit Algorithms - HasAVX512BF16 bool // Advanced vector extension 512 BFloat16 Instructions - HasAMXTile bool // Advanced Matrix Extension Tile instructions - HasAMXInt8 bool // Advanced Matrix Extension Int8 instructions - HasAMXBF16 bool // Advanced Matrix Extension BFloat16 instructions - HasBMI1 bool // Bit manipulation instruction set 1 - HasBMI2 bool // Bit manipulation instruction set 2 - HasCX16 bool // Compare and exchange 16 Bytes - HasERMS bool // Enhanced REP for MOVSB and STOSB - HasFMA bool // Fused-multiply-add instructions - HasOSXSAVE bool // OS supports XSAVE/XRESTOR for saving/restoring XMM registers. - HasPCLMULQDQ bool // PCLMULQDQ instruction - most often used for AES-GCM - HasPOPCNT bool // Hamming weight instruction POPCNT. - HasRDRAND bool // RDRAND instruction (on-chip random number generator) - HasRDSEED bool // RDSEED instruction (on-chip random number generator) - HasSSE2 bool // Streaming SIMD extension 2 (always available on amd64) - HasSSE3 bool // Streaming SIMD extension 3 - HasSSSE3 bool // Supplemental streaming SIMD extension 3 - HasSSE41 bool // Streaming SIMD extension 4 and 4.1 - HasSSE42 bool // Streaming SIMD extension 4 and 4.2 - _ CacheLinePad -} - -// ARM64 contains the supported CPU features of the -// current ARMv8(aarch64) platform. If the current platform -// is not arm64 then all feature flags are false. -var ARM64 struct { - _ CacheLinePad - HasFP bool // Floating-point instruction set (always available) - HasASIMD bool // Advanced SIMD (always available) - HasEVTSTRM bool // Event stream support - HasAES bool // AES hardware implementation - HasPMULL bool // Polynomial multiplication instruction set - HasSHA1 bool // SHA1 hardware implementation - HasSHA2 bool // SHA2 hardware implementation - HasCRC32 bool // CRC32 hardware implementation - HasATOMICS bool // Atomic memory operation instruction set - HasFPHP bool // Half precision floating-point instruction set - HasASIMDHP bool // Advanced SIMD half precision instruction set - HasCPUID bool // CPUID identification scheme registers - HasASIMDRDM bool // Rounding double multiply add/subtract instruction set - HasJSCVT bool // Javascript conversion from floating-point to integer - HasFCMA bool // Floating-point multiplication and addition of complex numbers - HasLRCPC bool // Release Consistent processor consistent support - HasDCPOP bool // Persistent memory support - HasSHA3 bool // SHA3 hardware implementation - HasSM3 bool // SM3 hardware implementation - HasSM4 bool // SM4 hardware implementation - HasASIMDDP bool // Advanced SIMD double precision instruction set - HasSHA512 bool // SHA512 hardware implementation - HasSVE bool // Scalable Vector Extensions - HasASIMDFHM bool // Advanced SIMD multiplication FP16 to FP32 - _ CacheLinePad -} - -// ARM contains the supported CPU features of the current ARM (32-bit) platform. -// All feature flags are false if: -// 1. the current platform is not arm, or -// 2. the current operating system is not Linux. -var ARM struct { - _ CacheLinePad - HasSWP bool // SWP instruction support - HasHALF bool // Half-word load and store support - HasTHUMB bool // ARM Thumb instruction set - Has26BIT bool // Address space limited to 26-bits - HasFASTMUL bool // 32-bit operand, 64-bit result multiplication support - HasFPA bool // Floating point arithmetic support - HasVFP bool // Vector floating point support - HasEDSP bool // DSP Extensions support - HasJAVA bool // Java instruction set - HasIWMMXT bool // Intel Wireless MMX technology support - HasCRUNCH bool // MaverickCrunch context switching and handling - HasTHUMBEE bool // Thumb EE instruction set - HasNEON bool // NEON instruction set - HasVFPv3 bool // Vector floating point version 3 support - HasVFPv3D16 bool // Vector floating point version 3 D8-D15 - HasTLS bool // Thread local storage support - HasVFPv4 bool // Vector floating point version 4 support - HasIDIVA bool // Integer divide instruction support in ARM mode - HasIDIVT bool // Integer divide instruction support in Thumb mode - HasVFPD32 bool // Vector floating point version 3 D15-D31 - HasLPAE bool // Large Physical Address Extensions - HasEVTSTRM bool // Event stream support - HasAES bool // AES hardware implementation - HasPMULL bool // Polynomial multiplication instruction set - HasSHA1 bool // SHA1 hardware implementation - HasSHA2 bool // SHA2 hardware implementation - HasCRC32 bool // CRC32 hardware implementation - _ CacheLinePad -} - -// MIPS64X contains the supported CPU features of the current mips64/mips64le -// platforms. If the current platform is not mips64/mips64le or the current -// operating system is not Linux then all feature flags are false. -var MIPS64X struct { - _ CacheLinePad - HasMSA bool // MIPS SIMD architecture - _ CacheLinePad -} - -// PPC64 contains the supported CPU features of the current ppc64/ppc64le platforms. -// If the current platform is not ppc64/ppc64le then all feature flags are false. -// -// For ppc64/ppc64le, it is safe to check only for ISA level starting on ISA v3.00, -// since there are no optional categories. There are some exceptions that also -// require kernel support to work (DARN, SCV), so there are feature bits for -// those as well. The struct is padded to avoid false sharing. -var PPC64 struct { - _ CacheLinePad - HasDARN bool // Hardware random number generator (requires kernel enablement) - HasSCV bool // Syscall vectored (requires kernel enablement) - IsPOWER8 bool // ISA v2.07 (POWER8) - IsPOWER9 bool // ISA v3.00 (POWER9), implies IsPOWER8 - _ CacheLinePad -} - -// S390X contains the supported CPU features of the current IBM Z -// (s390x) platform. If the current platform is not IBM Z then all -// feature flags are false. -// -// S390X is padded to avoid false sharing. Further HasVX is only set -// if the OS supports vector registers in addition to the STFLE -// feature bit being set. -var S390X struct { - _ CacheLinePad - HasZARCH bool // z/Architecture mode is active [mandatory] - HasSTFLE bool // store facility list extended - HasLDISP bool // long (20-bit) displacements - HasEIMM bool // 32-bit immediates - HasDFP bool // decimal floating point - HasETF3EH bool // ETF-3 enhanced - HasMSA bool // message security assist (CPACF) - HasAES bool // KM-AES{128,192,256} functions - HasAESCBC bool // KMC-AES{128,192,256} functions - HasAESCTR bool // KMCTR-AES{128,192,256} functions - HasAESGCM bool // KMA-GCM-AES{128,192,256} functions - HasGHASH bool // KIMD-GHASH function - HasSHA1 bool // K{I,L}MD-SHA-1 functions - HasSHA256 bool // K{I,L}MD-SHA-256 functions - HasSHA512 bool // K{I,L}MD-SHA-512 functions - HasSHA3 bool // K{I,L}MD-SHA3-{224,256,384,512} and K{I,L}MD-SHAKE-{128,256} functions - HasVX bool // vector facility - HasVXE bool // vector-enhancements facility 1 - _ CacheLinePad -} - -func init() { - archInit() - initOptions() - processOptions() -} - -// options contains the cpu debug options that can be used in GODEBUG. -// Options are arch dependent and are added by the arch specific initOptions functions. -// Features that are mandatory for the specific GOARCH should have the Required field set -// (e.g. SSE2 on amd64). -var options []option - -// Option names should be lower case. e.g. avx instead of AVX. -type option struct { - Name string - Feature *bool - Specified bool // whether feature value was specified in GODEBUG - Enable bool // whether feature should be enabled - Required bool // whether feature is mandatory and can not be disabled -} - -func processOptions() { - env := os.Getenv("GODEBUG") -field: - for env != "" { - field := "" - i := strings.IndexByte(env, ',') - if i < 0 { - field, env = env, "" - } else { - field, env = env[:i], env[i+1:] - } - if len(field) < 4 || field[:4] != "cpu." { - continue - } - i = strings.IndexByte(field, '=') - if i < 0 { - print("GODEBUG sys/cpu: no value specified for \"", field, "\"\n") - continue - } - key, value := field[4:i], field[i+1:] // e.g. "SSE2", "on" - - var enable bool - switch value { - case "on": - enable = true - case "off": - enable = false - default: - print("GODEBUG sys/cpu: value \"", value, "\" not supported for cpu option \"", key, "\"\n") - continue field - } - - if key == "all" { - for i := range options { - options[i].Specified = true - options[i].Enable = enable || options[i].Required - } - continue field - } - - for i := range options { - if options[i].Name == key { - options[i].Specified = true - options[i].Enable = enable - continue field - } - } - - print("GODEBUG sys/cpu: unknown cpu feature \"", key, "\"\n") - } - - for _, o := range options { - if !o.Specified { - continue - } - - if o.Enable && !*o.Feature { - print("GODEBUG sys/cpu: can not enable \"", o.Name, "\", missing CPU support\n") - continue - } - - if !o.Enable && o.Required { - print("GODEBUG sys/cpu: can not disable \"", o.Name, "\", required CPU feature\n") - continue - } - - *o.Feature = o.Enable - } -} diff --git a/internal/xcpu/cpu_aix.go b/internal/xcpu/cpu_aix.go deleted file mode 100644 index 5e6e2583..00000000 --- a/internal/xcpu/cpu_aix.go +++ /dev/null @@ -1,33 +0,0 @@ -// Copyright 2019 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. - -//go:build aix - -package xcpu - -const ( - // getsystemcfg constants - _SC_IMPL = 2 - _IMPL_POWER8 = 0x10000 - _IMPL_POWER9 = 0x20000 -) - -func archInit() { - impl := getsystemcfg(_SC_IMPL) - if impl&_IMPL_POWER8 != 0 { - PPC64.IsPOWER8 = true - } - if impl&_IMPL_POWER9 != 0 { - PPC64.IsPOWER8 = true - PPC64.IsPOWER9 = true - } - - Initialized = true -} - -func getsystemcfg(label int) (n uint64) { - r0, _ := callgetsystemcfg(label) - n = uint64(r0) - return -} diff --git a/internal/xcpu/cpu_arm.go b/internal/xcpu/cpu_arm.go deleted file mode 100644 index ff120458..00000000 --- a/internal/xcpu/cpu_arm.go +++ /dev/null @@ -1,73 +0,0 @@ -// Copyright 2018 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 xcpu - -const cacheLineSize = 32 - -// HWCAP/HWCAP2 bits. -// These are specific to Linux. -const ( - hwcap_SWP = 1 << 0 - hwcap_HALF = 1 << 1 - hwcap_THUMB = 1 << 2 - hwcap_26BIT = 1 << 3 - hwcap_FAST_MULT = 1 << 4 - hwcap_FPA = 1 << 5 - hwcap_VFP = 1 << 6 - hwcap_EDSP = 1 << 7 - hwcap_JAVA = 1 << 8 - hwcap_IWMMXT = 1 << 9 - hwcap_CRUNCH = 1 << 10 - hwcap_THUMBEE = 1 << 11 - hwcap_NEON = 1 << 12 - hwcap_VFPv3 = 1 << 13 - hwcap_VFPv3D16 = 1 << 14 - hwcap_TLS = 1 << 15 - hwcap_VFPv4 = 1 << 16 - hwcap_IDIVA = 1 << 17 - hwcap_IDIVT = 1 << 18 - hwcap_VFPD32 = 1 << 19 - hwcap_LPAE = 1 << 20 - hwcap_EVTSTRM = 1 << 21 - - hwcap2_AES = 1 << 0 - hwcap2_PMULL = 1 << 1 - hwcap2_SHA1 = 1 << 2 - hwcap2_SHA2 = 1 << 3 - hwcap2_CRC32 = 1 << 4 -) - -func initOptions() { - options = []option{ - {Name: "pmull", Feature: &ARM.HasPMULL}, - {Name: "sha1", Feature: &ARM.HasSHA1}, - {Name: "sha2", Feature: &ARM.HasSHA2}, - {Name: "swp", Feature: &ARM.HasSWP}, - {Name: "thumb", Feature: &ARM.HasTHUMB}, - {Name: "thumbee", Feature: &ARM.HasTHUMBEE}, - {Name: "tls", Feature: &ARM.HasTLS}, - {Name: "vfp", Feature: &ARM.HasVFP}, - {Name: "vfpd32", Feature: &ARM.HasVFPD32}, - {Name: "vfpv3", Feature: &ARM.HasVFPv3}, - {Name: "vfpv3d16", Feature: &ARM.HasVFPv3D16}, - {Name: "vfpv4", Feature: &ARM.HasVFPv4}, - {Name: "half", Feature: &ARM.HasHALF}, - {Name: "26bit", Feature: &ARM.Has26BIT}, - {Name: "fastmul", Feature: &ARM.HasFASTMUL}, - {Name: "fpa", Feature: &ARM.HasFPA}, - {Name: "edsp", Feature: &ARM.HasEDSP}, - {Name: "java", Feature: &ARM.HasJAVA}, - {Name: "iwmmxt", Feature: &ARM.HasIWMMXT}, - {Name: "crunch", Feature: &ARM.HasCRUNCH}, - {Name: "neon", Feature: &ARM.HasNEON}, - {Name: "idivt", Feature: &ARM.HasIDIVT}, - {Name: "idiva", Feature: &ARM.HasIDIVA}, - {Name: "lpae", Feature: &ARM.HasLPAE}, - {Name: "evtstrm", Feature: &ARM.HasEVTSTRM}, - {Name: "aes", Feature: &ARM.HasAES}, - {Name: "crc32", Feature: &ARM.HasCRC32}, - } - -} diff --git a/internal/xcpu/cpu_arm64.go b/internal/xcpu/cpu_arm64.go deleted file mode 100644 index 3d4113a5..00000000 --- a/internal/xcpu/cpu_arm64.go +++ /dev/null @@ -1,172 +0,0 @@ -// Copyright 2019 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 xcpu - -import "runtime" - -// cacheLineSize is used to prevent false sharing of cache lines. -// We choose 128 because Apple Silicon, a.k.a. M1, has 128-byte cache line size. -// It doesn't cost much and is much more future-proof. -const cacheLineSize = 128 - -func initOptions() { - options = []option{ - {Name: "fp", Feature: &ARM64.HasFP}, - {Name: "asimd", Feature: &ARM64.HasASIMD}, - {Name: "evstrm", Feature: &ARM64.HasEVTSTRM}, - {Name: "aes", Feature: &ARM64.HasAES}, - {Name: "fphp", Feature: &ARM64.HasFPHP}, - {Name: "jscvt", Feature: &ARM64.HasJSCVT}, - {Name: "lrcpc", Feature: &ARM64.HasLRCPC}, - {Name: "pmull", Feature: &ARM64.HasPMULL}, - {Name: "sha1", Feature: &ARM64.HasSHA1}, - {Name: "sha2", Feature: &ARM64.HasSHA2}, - {Name: "sha3", Feature: &ARM64.HasSHA3}, - {Name: "sha512", Feature: &ARM64.HasSHA512}, - {Name: "sm3", Feature: &ARM64.HasSM3}, - {Name: "sm4", Feature: &ARM64.HasSM4}, - {Name: "sve", Feature: &ARM64.HasSVE}, - {Name: "crc32", Feature: &ARM64.HasCRC32}, - {Name: "atomics", Feature: &ARM64.HasATOMICS}, - {Name: "asimdhp", Feature: &ARM64.HasASIMDHP}, - {Name: "cpuid", Feature: &ARM64.HasCPUID}, - {Name: "asimrdm", Feature: &ARM64.HasASIMDRDM}, - {Name: "fcma", Feature: &ARM64.HasFCMA}, - {Name: "dcpop", Feature: &ARM64.HasDCPOP}, - {Name: "asimddp", Feature: &ARM64.HasASIMDDP}, - {Name: "asimdfhm", Feature: &ARM64.HasASIMDFHM}, - } -} - -func archInit() { - switch runtime.GOOS { - case "freebsd": - readARM64Registers() - case "linux", "netbsd", "openbsd": - doinit() - default: - // Many platforms don't seem to allow reading these registers. - setMinimalFeatures() - } -} - -// setMinimalFeatures fakes the minimal ARM64 features expected by -// TestARM64minimalFeatures. -func setMinimalFeatures() { - ARM64.HasASIMD = true - ARM64.HasFP = true -} - -func readARM64Registers() { - Initialized = true - - parseARM64SystemRegisters(getisar0(), getisar1(), getpfr0()) -} - -func parseARM64SystemRegisters(isar0, isar1, pfr0 uint64) { - // ID_AA64ISAR0_EL1 - switch extractBits(isar0, 4, 7) { - case 1: - ARM64.HasAES = true - case 2: - ARM64.HasAES = true - ARM64.HasPMULL = true - } - - switch extractBits(isar0, 8, 11) { - case 1: - ARM64.HasSHA1 = true - } - - switch extractBits(isar0, 12, 15) { - case 1: - ARM64.HasSHA2 = true - case 2: - ARM64.HasSHA2 = true - ARM64.HasSHA512 = true - } - - switch extractBits(isar0, 16, 19) { - case 1: - ARM64.HasCRC32 = true - } - - switch extractBits(isar0, 20, 23) { - case 2: - ARM64.HasATOMICS = true - } - - switch extractBits(isar0, 28, 31) { - case 1: - ARM64.HasASIMDRDM = true - } - - switch extractBits(isar0, 32, 35) { - case 1: - ARM64.HasSHA3 = true - } - - switch extractBits(isar0, 36, 39) { - case 1: - ARM64.HasSM3 = true - } - - switch extractBits(isar0, 40, 43) { - case 1: - ARM64.HasSM4 = true - } - - switch extractBits(isar0, 44, 47) { - case 1: - ARM64.HasASIMDDP = true - } - - // ID_AA64ISAR1_EL1 - switch extractBits(isar1, 0, 3) { - case 1: - ARM64.HasDCPOP = true - } - - switch extractBits(isar1, 12, 15) { - case 1: - ARM64.HasJSCVT = true - } - - switch extractBits(isar1, 16, 19) { - case 1: - ARM64.HasFCMA = true - } - - switch extractBits(isar1, 20, 23) { - case 1: - ARM64.HasLRCPC = true - } - - // ID_AA64PFR0_EL1 - switch extractBits(pfr0, 16, 19) { - case 0: - ARM64.HasFP = true - case 1: - ARM64.HasFP = true - ARM64.HasFPHP = true - } - - switch extractBits(pfr0, 20, 23) { - case 0: - ARM64.HasASIMD = true - case 1: - ARM64.HasASIMD = true - ARM64.HasASIMDHP = true - } - - switch extractBits(pfr0, 32, 35) { - case 1: - ARM64.HasSVE = true - } -} - -func extractBits(data uint64, start, end uint) uint { - return (uint)(data>>start) & ((1 << (end - start + 1)) - 1) -} diff --git a/internal/xcpu/cpu_arm64.s b/internal/xcpu/cpu_arm64.s deleted file mode 100644 index fcb9a388..00000000 --- a/internal/xcpu/cpu_arm64.s +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2019 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. - -//go:build gc - -#include "textflag.h" - -// func getisar0() uint64 -TEXT ·getisar0(SB),NOSPLIT,$0-8 - // get Instruction Set Attributes 0 into x0 - // mrs x0, ID_AA64ISAR0_EL1 = d5380600 - WORD $0xd5380600 - MOVD R0, ret+0(FP) - RET - -// func getisar1() uint64 -TEXT ·getisar1(SB),NOSPLIT,$0-8 - // get Instruction Set Attributes 1 into x0 - // mrs x0, ID_AA64ISAR1_EL1 = d5380620 - WORD $0xd5380620 - MOVD R0, ret+0(FP) - RET - -// func getpfr0() uint64 -TEXT ·getpfr0(SB),NOSPLIT,$0-8 - // get Processor Feature Register 0 into x0 - // mrs x0, ID_AA64PFR0_EL1 = d5380400 - WORD $0xd5380400 - MOVD R0, ret+0(FP) - RET diff --git a/internal/xcpu/cpu_gc_arm64.go b/internal/xcpu/cpu_gc_arm64.go deleted file mode 100644 index 26d3050d..00000000 --- a/internal/xcpu/cpu_gc_arm64.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 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. - -//go:build gc - -package xcpu - -func getisar0() uint64 -func getisar1() uint64 -func getpfr0() uint64 diff --git a/internal/xcpu/cpu_gc_s390x.go b/internal/xcpu/cpu_gc_s390x.go deleted file mode 100644 index 34ca88b7..00000000 --- a/internal/xcpu/cpu_gc_s390x.go +++ /dev/null @@ -1,21 +0,0 @@ -// Copyright 2019 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. - -//go:build gc - -package xcpu - -// haveAsmFunctions reports whether the other functions in this file can -// be safely called. -func haveAsmFunctions() bool { return true } - -// The following feature detection functions are defined in cpu_s390x.s. -// They are likely to be expensive to call so the results should be cached. -func stfle() facilityList -func kmQuery() queryResult -func kmcQuery() queryResult -func kmctrQuery() queryResult -func kmaQuery() queryResult -func kimdQuery() queryResult -func klmdQuery() queryResult diff --git a/internal/xcpu/cpu_gc_x86.go b/internal/xcpu/cpu_gc_x86.go deleted file mode 100644 index 9d6f61c2..00000000 --- a/internal/xcpu/cpu_gc_x86.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2018 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. - -//go:build (386 || amd64 || amd64p32) && gc - -package xcpu - -// cpuid is implemented in cpu_x86.s for gc compiler -// and in cpu_gccgo.c for gccgo. -func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) - -// xgetbv with ecx = 0 is implemented in cpu_x86.s for gc compiler -// and in cpu_gccgo.c for gccgo. -func xgetbv() (eax, edx uint32) diff --git a/internal/xcpu/cpu_gccgo_arm64.go b/internal/xcpu/cpu_gccgo_arm64.go deleted file mode 100644 index d6c2a3a8..00000000 --- a/internal/xcpu/cpu_gccgo_arm64.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 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. - -//go:build gccgo - -package xcpu - -func getisar0() uint64 { return 0 } -func getisar1() uint64 { return 0 } -func getpfr0() uint64 { return 0 } diff --git a/internal/xcpu/cpu_gccgo_s390x.go b/internal/xcpu/cpu_gccgo_s390x.go deleted file mode 100644 index 4deec625..00000000 --- a/internal/xcpu/cpu_gccgo_s390x.go +++ /dev/null @@ -1,22 +0,0 @@ -// Copyright 2019 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. - -//go:build gccgo - -package xcpu - -// haveAsmFunctions reports whether the other functions in this file can -// be safely called. -func haveAsmFunctions() bool { return false } - -// TODO(mundaym): the following feature detection functions are currently -// stubs. See https://golang.org/cl/162887 for how to fix this. -// They are likely to be expensive to call so the results should be cached. -func stfle() facilityList { panic("not implemented for gccgo") } -func kmQuery() queryResult { panic("not implemented for gccgo") } -func kmcQuery() queryResult { panic("not implemented for gccgo") } -func kmctrQuery() queryResult { panic("not implemented for gccgo") } -func kmaQuery() queryResult { panic("not implemented for gccgo") } -func kimdQuery() queryResult { panic("not implemented for gccgo") } -func klmdQuery() queryResult { panic("not implemented for gccgo") } diff --git a/internal/xcpu/cpu_gccgo_x86.c b/internal/xcpu/cpu_gccgo_x86.c deleted file mode 100644 index 3f73a05d..00000000 --- a/internal/xcpu/cpu_gccgo_x86.c +++ /dev/null @@ -1,37 +0,0 @@ -// Copyright 2018 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. - -//go:build (386 || amd64 || amd64p32) && gccgo - -#include -#include -#include - -// Need to wrap __get_cpuid_count because it's declared as static. -int -gccgoGetCpuidCount(uint32_t leaf, uint32_t subleaf, - uint32_t *eax, uint32_t *ebx, - uint32_t *ecx, uint32_t *edx) -{ - return __get_cpuid_count(leaf, subleaf, eax, ebx, ecx, edx); -} - -#pragma GCC diagnostic ignored "-Wunknown-pragmas" -#pragma GCC push_options -#pragma GCC target("xsave") -#pragma clang attribute push (__attribute__((target("xsave"))), apply_to=function) - -// xgetbv reads the contents of an XCR (Extended Control Register) -// specified in the ECX register into registers EDX:EAX. -// Currently, the only supported value for XCR is 0. -void -gccgoXgetbv(uint32_t *eax, uint32_t *edx) -{ - uint64_t v = _xgetbv(0); - *eax = v & 0xffffffff; - *edx = v >> 32; -} - -#pragma clang attribute pop -#pragma GCC pop_options diff --git a/internal/xcpu/cpu_gccgo_x86.go b/internal/xcpu/cpu_gccgo_x86.go deleted file mode 100644 index e66c6ee9..00000000 --- a/internal/xcpu/cpu_gccgo_x86.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright 2018 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. - -//go:build (386 || amd64 || amd64p32) && gccgo - -package xcpu - -//extern gccgoGetCpuidCount -func gccgoGetCpuidCount(eaxArg, ecxArg uint32, eax, ebx, ecx, edx *uint32) - -func cpuid(eaxArg, ecxArg uint32) (eax, ebx, ecx, edx uint32) { - var a, b, c, d uint32 - gccgoGetCpuidCount(eaxArg, ecxArg, &a, &b, &c, &d) - return a, b, c, d -} - -//extern gccgoXgetbv -func gccgoXgetbv(eax, edx *uint32) - -func xgetbv() (eax, edx uint32) { - var a, d uint32 - gccgoXgetbv(&a, &d) - return a, d -} - -// gccgo doesn't build on Darwin, per: -// https://github.com/Homebrew/homebrew-core/blob/HEAD/Formula/gcc.rb#L76 -func darwinSupportsAVX512() bool { - return false -} diff --git a/internal/xcpu/cpu_linux.go b/internal/xcpu/cpu_linux.go deleted file mode 100644 index 10a48916..00000000 --- a/internal/xcpu/cpu_linux.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2018 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. - -//go:build !386 && !amd64 && !amd64p32 && !arm64 - -package xcpu - -func archInit() { - if err := readHWCAP(); err != nil { - return - } - doinit() - Initialized = true -} diff --git a/internal/xcpu/cpu_linux_arm.go b/internal/xcpu/cpu_linux_arm.go deleted file mode 100644 index 28e32637..00000000 --- a/internal/xcpu/cpu_linux_arm.go +++ /dev/null @@ -1,39 +0,0 @@ -// Copyright 2019 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 xcpu - -func doinit() { - ARM.HasSWP = isSet(hwCap, hwcap_SWP) - ARM.HasHALF = isSet(hwCap, hwcap_HALF) - ARM.HasTHUMB = isSet(hwCap, hwcap_THUMB) - ARM.Has26BIT = isSet(hwCap, hwcap_26BIT) - ARM.HasFASTMUL = isSet(hwCap, hwcap_FAST_MULT) - ARM.HasFPA = isSet(hwCap, hwcap_FPA) - ARM.HasVFP = isSet(hwCap, hwcap_VFP) - ARM.HasEDSP = isSet(hwCap, hwcap_EDSP) - ARM.HasJAVA = isSet(hwCap, hwcap_JAVA) - ARM.HasIWMMXT = isSet(hwCap, hwcap_IWMMXT) - ARM.HasCRUNCH = isSet(hwCap, hwcap_CRUNCH) - ARM.HasTHUMBEE = isSet(hwCap, hwcap_THUMBEE) - ARM.HasNEON = isSet(hwCap, hwcap_NEON) - ARM.HasVFPv3 = isSet(hwCap, hwcap_VFPv3) - ARM.HasVFPv3D16 = isSet(hwCap, hwcap_VFPv3D16) - ARM.HasTLS = isSet(hwCap, hwcap_TLS) - ARM.HasVFPv4 = isSet(hwCap, hwcap_VFPv4) - ARM.HasIDIVA = isSet(hwCap, hwcap_IDIVA) - ARM.HasIDIVT = isSet(hwCap, hwcap_IDIVT) - ARM.HasVFPD32 = isSet(hwCap, hwcap_VFPD32) - ARM.HasLPAE = isSet(hwCap, hwcap_LPAE) - ARM.HasEVTSTRM = isSet(hwCap, hwcap_EVTSTRM) - ARM.HasAES = isSet(hwCap2, hwcap2_AES) - ARM.HasPMULL = isSet(hwCap2, hwcap2_PMULL) - ARM.HasSHA1 = isSet(hwCap2, hwcap2_SHA1) - ARM.HasSHA2 = isSet(hwCap2, hwcap2_SHA2) - ARM.HasCRC32 = isSet(hwCap2, hwcap2_CRC32) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/internal/xcpu/cpu_linux_arm64.go b/internal/xcpu/cpu_linux_arm64.go deleted file mode 100644 index 481f450b..00000000 --- a/internal/xcpu/cpu_linux_arm64.go +++ /dev/null @@ -1,111 +0,0 @@ -// Copyright 2018 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 xcpu - -import ( - "strings" - "syscall" -) - -// HWCAP/HWCAP2 bits. These are exposed by Linux. -const ( - hwcap_FP = 1 << 0 - hwcap_ASIMD = 1 << 1 - hwcap_EVTSTRM = 1 << 2 - hwcap_AES = 1 << 3 - hwcap_PMULL = 1 << 4 - hwcap_SHA1 = 1 << 5 - hwcap_SHA2 = 1 << 6 - hwcap_CRC32 = 1 << 7 - hwcap_ATOMICS = 1 << 8 - hwcap_FPHP = 1 << 9 - hwcap_ASIMDHP = 1 << 10 - hwcap_CPUID = 1 << 11 - hwcap_ASIMDRDM = 1 << 12 - hwcap_JSCVT = 1 << 13 - hwcap_FCMA = 1 << 14 - hwcap_LRCPC = 1 << 15 - hwcap_DCPOP = 1 << 16 - hwcap_SHA3 = 1 << 17 - hwcap_SM3 = 1 << 18 - hwcap_SM4 = 1 << 19 - hwcap_ASIMDDP = 1 << 20 - hwcap_SHA512 = 1 << 21 - hwcap_SVE = 1 << 22 - hwcap_ASIMDFHM = 1 << 23 -) - -// linuxKernelCanEmulateCPUID reports whether we're running -// on Linux 4.11+. Ideally we'd like to ask the question about -// whether the current kernel contains -// https://git.kernel.org/pub/scm/linux/kernel/git/torvalds/linux.git/commit/?id=77c97b4ee21290f5f083173d957843b615abbff2 -// but the version number will have to do. -func linuxKernelCanEmulateCPUID() bool { - var un syscall.Utsname - syscall.Uname(&un) - var sb strings.Builder - for _, b := range un.Release[:] { - if b == 0 { - break - } - sb.WriteByte(byte(b)) - } - major, minor, _, ok := parseRelease(sb.String()) - return ok && (major > 4 || major == 4 && minor >= 11) -} - -func doinit() { - if err := readHWCAP(); err != nil { - // We failed to read /proc/self/auxv. This can happen if the binary has - // been given extra capabilities(7) with /bin/setcap. - // - // When this happens, we have two options. If the Linux kernel is new - // enough (4.11+), we can read the arm64 registers directly which'll - // trap into the kernel and then return back to userspace. - // - // But on older kernels, such as Linux 4.4.180 as used on many Synology - // devices, calling readARM64Registers (specifically getisar0) will - // cause a SIGILL and we'll die. So for older kernels, parse /proc/cpuinfo - // instead. - // - // See golang/go#57336. - if linuxKernelCanEmulateCPUID() { - readARM64Registers() - } else { - readLinuxProcCPUInfo() - } - return - } - - // HWCAP feature bits - ARM64.HasFP = isSet(hwCap, hwcap_FP) - ARM64.HasASIMD = isSet(hwCap, hwcap_ASIMD) - ARM64.HasEVTSTRM = isSet(hwCap, hwcap_EVTSTRM) - ARM64.HasAES = isSet(hwCap, hwcap_AES) - ARM64.HasPMULL = isSet(hwCap, hwcap_PMULL) - ARM64.HasSHA1 = isSet(hwCap, hwcap_SHA1) - ARM64.HasSHA2 = isSet(hwCap, hwcap_SHA2) - ARM64.HasCRC32 = isSet(hwCap, hwcap_CRC32) - ARM64.HasATOMICS = isSet(hwCap, hwcap_ATOMICS) - ARM64.HasFPHP = isSet(hwCap, hwcap_FPHP) - ARM64.HasASIMDHP = isSet(hwCap, hwcap_ASIMDHP) - ARM64.HasCPUID = isSet(hwCap, hwcap_CPUID) - ARM64.HasASIMDRDM = isSet(hwCap, hwcap_ASIMDRDM) - ARM64.HasJSCVT = isSet(hwCap, hwcap_JSCVT) - ARM64.HasFCMA = isSet(hwCap, hwcap_FCMA) - ARM64.HasLRCPC = isSet(hwCap, hwcap_LRCPC) - ARM64.HasDCPOP = isSet(hwCap, hwcap_DCPOP) - ARM64.HasSHA3 = isSet(hwCap, hwcap_SHA3) - ARM64.HasSM3 = isSet(hwCap, hwcap_SM3) - ARM64.HasSM4 = isSet(hwCap, hwcap_SM4) - ARM64.HasASIMDDP = isSet(hwCap, hwcap_ASIMDDP) - ARM64.HasSHA512 = isSet(hwCap, hwcap_SHA512) - ARM64.HasSVE = isSet(hwCap, hwcap_SVE) - ARM64.HasASIMDFHM = isSet(hwCap, hwcap_ASIMDFHM) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/internal/xcpu/cpu_linux_mips64x.go b/internal/xcpu/cpu_linux_mips64x.go deleted file mode 100644 index 15fdee9c..00000000 --- a/internal/xcpu/cpu_linux_mips64x.go +++ /dev/null @@ -1,22 +0,0 @@ -// 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. - -//go:build linux && (mips64 || mips64le) - -package xcpu - -// HWCAP bits. These are exposed by the Linux kernel 5.4. -const ( - // CPU features - hwcap_MIPS_MSA = 1 << 1 -) - -func doinit() { - // HWCAP feature bits - MIPS64X.HasMSA = isSet(hwCap, hwcap_MIPS_MSA) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/internal/xcpu/cpu_linux_noinit.go b/internal/xcpu/cpu_linux_noinit.go deleted file mode 100644 index 878e56fb..00000000 --- a/internal/xcpu/cpu_linux_noinit.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2019 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. - -//go:build linux && !arm && !arm64 && !mips64 && !mips64le && !ppc64 && !ppc64le && !s390x - -package xcpu - -func doinit() {} diff --git a/internal/xcpu/cpu_linux_ppc64x.go b/internal/xcpu/cpu_linux_ppc64x.go deleted file mode 100644 index 6a8ea12a..00000000 --- a/internal/xcpu/cpu_linux_ppc64x.go +++ /dev/null @@ -1,30 +0,0 @@ -// Copyright 2018 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. - -//go:build linux && (ppc64 || ppc64le) - -package xcpu - -// HWCAP/HWCAP2 bits. These are exposed by the kernel. -const ( - // ISA Level - _PPC_FEATURE2_ARCH_2_07 = 0x80000000 - _PPC_FEATURE2_ARCH_3_00 = 0x00800000 - - // CPU features - _PPC_FEATURE2_DARN = 0x00200000 - _PPC_FEATURE2_SCV = 0x00100000 -) - -func doinit() { - // HWCAP2 feature bits - PPC64.IsPOWER8 = isSet(hwCap2, _PPC_FEATURE2_ARCH_2_07) - PPC64.IsPOWER9 = isSet(hwCap2, _PPC_FEATURE2_ARCH_3_00) - PPC64.HasDARN = isSet(hwCap2, _PPC_FEATURE2_DARN) - PPC64.HasSCV = isSet(hwCap2, _PPC_FEATURE2_SCV) -} - -func isSet(hwc uint, value uint) bool { - return hwc&value != 0 -} diff --git a/internal/xcpu/cpu_linux_s390x.go b/internal/xcpu/cpu_linux_s390x.go deleted file mode 100644 index ff0ca7f4..00000000 --- a/internal/xcpu/cpu_linux_s390x.go +++ /dev/null @@ -1,40 +0,0 @@ -// Copyright 2019 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 xcpu - -const ( - // bit mask values from /usr/include/bits/hwcap.h - hwcap_ZARCH = 2 - hwcap_STFLE = 4 - hwcap_MSA = 8 - hwcap_LDISP = 16 - hwcap_EIMM = 32 - hwcap_DFP = 64 - hwcap_ETF3EH = 256 - hwcap_VX = 2048 - hwcap_VXE = 8192 -) - -func initS390Xbase() { - // test HWCAP bit vector - has := func(featureMask uint) bool { - return hwCap&featureMask == featureMask - } - - // mandatory - S390X.HasZARCH = has(hwcap_ZARCH) - - // optional - S390X.HasSTFLE = has(hwcap_STFLE) - S390X.HasLDISP = has(hwcap_LDISP) - S390X.HasEIMM = has(hwcap_EIMM) - S390X.HasETF3EH = has(hwcap_ETF3EH) - S390X.HasDFP = has(hwcap_DFP) - S390X.HasMSA = has(hwcap_MSA) - S390X.HasVX = has(hwcap_VX) - if S390X.HasVX { - S390X.HasVXE = has(hwcap_VXE) - } -} diff --git a/internal/xcpu/cpu_loong64.go b/internal/xcpu/cpu_loong64.go deleted file mode 100644 index fdb21c60..00000000 --- a/internal/xcpu/cpu_loong64.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 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. - -//go:build loong64 - -package xcpu - -const cacheLineSize = 64 - -func initOptions() { -} diff --git a/internal/xcpu/cpu_mips64x.go b/internal/xcpu/cpu_mips64x.go deleted file mode 100644 index 447fee98..00000000 --- a/internal/xcpu/cpu_mips64x.go +++ /dev/null @@ -1,15 +0,0 @@ -// Copyright 2018 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. - -//go:build mips64 || mips64le - -package xcpu - -const cacheLineSize = 32 - -func initOptions() { - options = []option{ - {Name: "msa", Feature: &MIPS64X.HasMSA}, - } -} diff --git a/internal/xcpu/cpu_mipsx.go b/internal/xcpu/cpu_mipsx.go deleted file mode 100644 index 6efa1917..00000000 --- a/internal/xcpu/cpu_mipsx.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2018 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. - -//go:build mips || mipsle - -package xcpu - -const cacheLineSize = 32 - -func initOptions() {} diff --git a/internal/xcpu/cpu_netbsd_arm64.go b/internal/xcpu/cpu_netbsd_arm64.go deleted file mode 100644 index b84b4408..00000000 --- a/internal/xcpu/cpu_netbsd_arm64.go +++ /dev/null @@ -1,173 +0,0 @@ -// 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 xcpu - -import ( - "syscall" - "unsafe" -) - -// Minimal copy of functionality from x/sys/unix so the cpu package can call -// sysctl without depending on x/sys/unix. - -const ( - _CTL_QUERY = -2 - - _SYSCTL_VERS_1 = 0x1000000 -) - -var _zero uintptr - -func sysctl(mib []int32, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) { - var _p0 unsafe.Pointer - if len(mib) > 0 { - _p0 = unsafe.Pointer(&mib[0]) - } else { - _p0 = unsafe.Pointer(&_zero) - } - _, _, errno := syscall.Syscall6( - syscall.SYS___SYSCTL, - uintptr(_p0), - uintptr(len(mib)), - uintptr(unsafe.Pointer(old)), - uintptr(unsafe.Pointer(oldlen)), - uintptr(unsafe.Pointer(new)), - uintptr(newlen)) - if errno != 0 { - return errno - } - return nil -} - -type sysctlNode struct { - Flags uint32 - Num int32 - Name [32]int8 - Ver uint32 - __rsvd uint32 - Un [16]byte - _sysctl_size [8]byte - _sysctl_func [8]byte - _sysctl_parent [8]byte - _sysctl_desc [8]byte -} - -func sysctlNodes(mib []int32) ([]sysctlNode, error) { - var olen uintptr - - // Get a list of all sysctl nodes below the given MIB by performing - // a sysctl for the given MIB with CTL_QUERY appended. - mib = append(mib, _CTL_QUERY) - qnode := sysctlNode{Flags: _SYSCTL_VERS_1} - qp := (*byte)(unsafe.Pointer(&qnode)) - sz := unsafe.Sizeof(qnode) - if err := sysctl(mib, nil, &olen, qp, sz); err != nil { - return nil, err - } - - // Now that we know the size, get the actual nodes. - nodes := make([]sysctlNode, olen/sz) - np := (*byte)(unsafe.Pointer(&nodes[0])) - if err := sysctl(mib, np, &olen, qp, sz); err != nil { - return nil, err - } - - return nodes, nil -} - -func nametomib(name string) ([]int32, error) { - // Split name into components. - var parts []string - last := 0 - for i := 0; i < len(name); i++ { - if name[i] == '.' { - parts = append(parts, name[last:i]) - last = i + 1 - } - } - parts = append(parts, name[last:]) - - mib := []int32{} - // Discover the nodes and construct the MIB OID. - for partno, part := range parts { - nodes, err := sysctlNodes(mib) - if err != nil { - return nil, err - } - for _, node := range nodes { - n := make([]byte, 0) - for i := range node.Name { - if node.Name[i] != 0 { - n = append(n, byte(node.Name[i])) - } - } - if string(n) == part { - mib = append(mib, int32(node.Num)) - break - } - } - if len(mib) != partno+1 { - return nil, err - } - } - - return mib, nil -} - -// aarch64SysctlCPUID is struct aarch64_sysctl_cpu_id from NetBSD's -type aarch64SysctlCPUID struct { - midr uint64 /* Main ID Register */ - revidr uint64 /* Revision ID Register */ - mpidr uint64 /* Multiprocessor Affinity Register */ - aa64dfr0 uint64 /* A64 Debug Feature Register 0 */ - aa64dfr1 uint64 /* A64 Debug Feature Register 1 */ - aa64isar0 uint64 /* A64 Instruction Set Attribute Register 0 */ - aa64isar1 uint64 /* A64 Instruction Set Attribute Register 1 */ - aa64mmfr0 uint64 /* A64 Memory Model Feature Register 0 */ - aa64mmfr1 uint64 /* A64 Memory Model Feature Register 1 */ - aa64mmfr2 uint64 /* A64 Memory Model Feature Register 2 */ - aa64pfr0 uint64 /* A64 Processor Feature Register 0 */ - aa64pfr1 uint64 /* A64 Processor Feature Register 1 */ - aa64zfr0 uint64 /* A64 SVE Feature ID Register 0 */ - mvfr0 uint32 /* Media and VFP Feature Register 0 */ - mvfr1 uint32 /* Media and VFP Feature Register 1 */ - mvfr2 uint32 /* Media and VFP Feature Register 2 */ - pad uint32 - clidr uint64 /* Cache Level ID Register */ - ctr uint64 /* Cache Type Register */ -} - -func sysctlCPUID(name string) (*aarch64SysctlCPUID, error) { - mib, err := nametomib(name) - if err != nil { - return nil, err - } - - out := aarch64SysctlCPUID{} - n := unsafe.Sizeof(out) - _, _, errno := syscall.Syscall6( - syscall.SYS___SYSCTL, - uintptr(unsafe.Pointer(&mib[0])), - uintptr(len(mib)), - uintptr(unsafe.Pointer(&out)), - uintptr(unsafe.Pointer(&n)), - uintptr(0), - uintptr(0)) - if errno != 0 { - return nil, errno - } - return &out, nil -} - -func doinit() { - cpuid, err := sysctlCPUID("machdep.cpu0.cpu_id") - if err != nil { - setMinimalFeatures() - return - } - parseARM64SystemRegisters(cpuid.aa64isar0, cpuid.aa64isar1, cpuid.aa64pfr0) - - Initialized = true -} diff --git a/internal/xcpu/cpu_openbsd_arm64.go b/internal/xcpu/cpu_openbsd_arm64.go deleted file mode 100644 index 2459a486..00000000 --- a/internal/xcpu/cpu_openbsd_arm64.go +++ /dev/null @@ -1,65 +0,0 @@ -// Copyright 2022 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 xcpu - -import ( - "syscall" - "unsafe" -) - -// Minimal copy of functionality from x/sys/unix so the cpu package can call -// sysctl without depending on x/sys/unix. - -const ( - // From OpenBSD's sys/sysctl.h. - _CTL_MACHDEP = 7 - - // From OpenBSD's machine/cpu.h. - _CPU_ID_AA64ISAR0 = 2 - _CPU_ID_AA64ISAR1 = 3 -) - -// Implemented in the runtime package (runtime/sys_openbsd3.go) -func syscall_syscall6(fn, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err syscall.Errno) - -//go:linkname syscall_syscall6 syscall.syscall6 - -func sysctl(mib []uint32, old *byte, oldlen *uintptr, new *byte, newlen uintptr) (err error) { - _, _, errno := syscall_syscall6(libc_sysctl_trampoline_addr, uintptr(unsafe.Pointer(&mib[0])), uintptr(len(mib)), uintptr(unsafe.Pointer(old)), uintptr(unsafe.Pointer(oldlen)), uintptr(unsafe.Pointer(new)), uintptr(newlen)) - if errno != 0 { - return errno - } - return nil -} - -var libc_sysctl_trampoline_addr uintptr - -//go:cgo_import_dynamic libc_sysctl sysctl "libc.so" - -func sysctlUint64(mib []uint32) (uint64, bool) { - var out uint64 - nout := unsafe.Sizeof(out) - if err := sysctl(mib, (*byte)(unsafe.Pointer(&out)), &nout, nil, 0); err != nil { - return 0, false - } - return out, true -} - -func doinit() { - setMinimalFeatures() - - // Get ID_AA64ISAR0 and ID_AA64ISAR1 from sysctl. - isar0, ok := sysctlUint64([]uint32{_CTL_MACHDEP, _CPU_ID_AA64ISAR0}) - if !ok { - return - } - isar1, ok := sysctlUint64([]uint32{_CTL_MACHDEP, _CPU_ID_AA64ISAR1}) - if !ok { - return - } - parseARM64SystemRegisters(isar0, isar1, 0) - - Initialized = true -} diff --git a/internal/xcpu/cpu_openbsd_arm64.s b/internal/xcpu/cpu_openbsd_arm64.s deleted file mode 100644 index 054ba05d..00000000 --- a/internal/xcpu/cpu_openbsd_arm64.s +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2022 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. - -#include "textflag.h" - -TEXT libc_sysctl_trampoline<>(SB),NOSPLIT,$0-0 - JMP libc_sysctl(SB) - -GLOBL ·libc_sysctl_trampoline_addr(SB), RODATA, $8 -DATA ·libc_sysctl_trampoline_addr(SB)/8, $libc_sysctl_trampoline<>(SB) diff --git a/internal/xcpu/cpu_other_arm.go b/internal/xcpu/cpu_other_arm.go deleted file mode 100644 index e3247948..00000000 --- a/internal/xcpu/cpu_other_arm.go +++ /dev/null @@ -1,9 +0,0 @@ -// 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. - -//go:build !linux && arm - -package xcpu - -func archInit() {} diff --git a/internal/xcpu/cpu_other_arm64.go b/internal/xcpu/cpu_other_arm64.go deleted file mode 100644 index 5257a0b6..00000000 --- a/internal/xcpu/cpu_other_arm64.go +++ /dev/null @@ -1,9 +0,0 @@ -// Copyright 2019 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. - -//go:build !linux && !netbsd && !openbsd && arm64 - -package xcpu - -func doinit() {} diff --git a/internal/xcpu/cpu_other_mips64x.go b/internal/xcpu/cpu_other_mips64x.go deleted file mode 100644 index b1ddc9d5..00000000 --- a/internal/xcpu/cpu_other_mips64x.go +++ /dev/null @@ -1,11 +0,0 @@ -// 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. - -//go:build !linux && (mips64 || mips64le) - -package xcpu - -func archInit() { - Initialized = true -} diff --git a/internal/xcpu/cpu_other_ppc64x.go b/internal/xcpu/cpu_other_ppc64x.go deleted file mode 100644 index 00a08baa..00000000 --- a/internal/xcpu/cpu_other_ppc64x.go +++ /dev/null @@ -1,12 +0,0 @@ -// Copyright 2022 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. - -//go:build !aix && !linux && (ppc64 || ppc64le) - -package xcpu - -func archInit() { - PPC64.IsPOWER8 = true - Initialized = true -} diff --git a/internal/xcpu/cpu_other_riscv64.go b/internal/xcpu/cpu_other_riscv64.go deleted file mode 100644 index 7f8fd1fc..00000000 --- a/internal/xcpu/cpu_other_riscv64.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2022 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. - -//go:build !linux && riscv64 - -package xcpu - -func archInit() { - Initialized = true -} diff --git a/internal/xcpu/cpu_ppc64x.go b/internal/xcpu/cpu_ppc64x.go deleted file mode 100644 index 22afeec2..00000000 --- a/internal/xcpu/cpu_ppc64x.go +++ /dev/null @@ -1,16 +0,0 @@ -// 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. - -//go:build ppc64 || ppc64le - -package xcpu - -const cacheLineSize = 128 - -func initOptions() { - options = []option{ - {Name: "darn", Feature: &PPC64.HasDARN}, - {Name: "scv", Feature: &PPC64.HasSCV}, - } -} diff --git a/internal/xcpu/cpu_riscv64.go b/internal/xcpu/cpu_riscv64.go deleted file mode 100644 index 28e57b68..00000000 --- a/internal/xcpu/cpu_riscv64.go +++ /dev/null @@ -1,11 +0,0 @@ -// Copyright 2019 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. - -//go:build riscv64 - -package xcpu - -const cacheLineSize = 64 - -func initOptions() {} diff --git a/internal/xcpu/cpu_s390x.go b/internal/xcpu/cpu_s390x.go deleted file mode 100644 index e85a8c5d..00000000 --- a/internal/xcpu/cpu_s390x.go +++ /dev/null @@ -1,172 +0,0 @@ -// 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 xcpu - -const cacheLineSize = 256 - -func initOptions() { - options = []option{ - {Name: "zarch", Feature: &S390X.HasZARCH, Required: true}, - {Name: "stfle", Feature: &S390X.HasSTFLE, Required: true}, - {Name: "ldisp", Feature: &S390X.HasLDISP, Required: true}, - {Name: "eimm", Feature: &S390X.HasEIMM, Required: true}, - {Name: "dfp", Feature: &S390X.HasDFP}, - {Name: "etf3eh", Feature: &S390X.HasETF3EH}, - {Name: "msa", Feature: &S390X.HasMSA}, - {Name: "aes", Feature: &S390X.HasAES}, - {Name: "aescbc", Feature: &S390X.HasAESCBC}, - {Name: "aesctr", Feature: &S390X.HasAESCTR}, - {Name: "aesgcm", Feature: &S390X.HasAESGCM}, - {Name: "ghash", Feature: &S390X.HasGHASH}, - {Name: "sha1", Feature: &S390X.HasSHA1}, - {Name: "sha256", Feature: &S390X.HasSHA256}, - {Name: "sha3", Feature: &S390X.HasSHA3}, - {Name: "sha512", Feature: &S390X.HasSHA512}, - {Name: "vx", Feature: &S390X.HasVX}, - {Name: "vxe", Feature: &S390X.HasVXE}, - } -} - -// bitIsSet reports whether the bit at index is set. The bit index -// is in big endian order, so bit index 0 is the leftmost bit. -func bitIsSet(bits []uint64, index uint) bool { - return bits[index/64]&((1<<63)>>(index%64)) != 0 -} - -// facility is a bit index for the named facility. -type facility uint8 - -const ( - // mandatory facilities - zarch facility = 1 // z architecture mode is active - stflef facility = 7 // store-facility-list-extended - ldisp facility = 18 // long-displacement - eimm facility = 21 // extended-immediate - - // miscellaneous facilities - dfp facility = 42 // decimal-floating-point - etf3eh facility = 30 // extended-translation 3 enhancement - - // cryptography facilities - msa facility = 17 // message-security-assist - msa3 facility = 76 // message-security-assist extension 3 - msa4 facility = 77 // message-security-assist extension 4 - msa5 facility = 57 // message-security-assist extension 5 - msa8 facility = 146 // message-security-assist extension 8 - msa9 facility = 155 // message-security-assist extension 9 - - // vector facilities - vx facility = 129 // vector facility - vxe facility = 135 // vector-enhancements 1 - vxe2 facility = 148 // vector-enhancements 2 -) - -// facilityList contains the result of an STFLE call. -// Bits are numbered in big endian order so the -// leftmost bit (the MSB) is at index 0. -type facilityList struct { - bits [4]uint64 -} - -// Has reports whether the given facilities are present. -func (s *facilityList) Has(fs ...facility) bool { - if len(fs) == 0 { - panic("no facility bits provided") - } - for _, f := range fs { - if !bitIsSet(s.bits[:], uint(f)) { - return false - } - } - return true -} - -// function is the code for the named cryptographic function. -type function uint8 - -const ( - // KM{,A,C,CTR} function codes - aes128 function = 18 // AES-128 - aes192 function = 19 // AES-192 - aes256 function = 20 // AES-256 - - // K{I,L}MD function codes - sha1 function = 1 // SHA-1 - sha256 function = 2 // SHA-256 - sha512 function = 3 // SHA-512 - sha3_224 function = 32 // SHA3-224 - sha3_256 function = 33 // SHA3-256 - sha3_384 function = 34 // SHA3-384 - sha3_512 function = 35 // SHA3-512 - shake128 function = 36 // SHAKE-128 - shake256 function = 37 // SHAKE-256 - - // KLMD function codes - ghash function = 65 // GHASH -) - -// queryResult contains the result of a Query function -// call. Bits are numbered in big endian order so the -// leftmost bit (the MSB) is at index 0. -type queryResult struct { - bits [2]uint64 -} - -// Has reports whether the given functions are present. -func (q *queryResult) Has(fns ...function) bool { - if len(fns) == 0 { - panic("no function codes provided") - } - for _, f := range fns { - if !bitIsSet(q.bits[:], uint(f)) { - return false - } - } - return true -} - -func doinit() { - initS390Xbase() - - // We need implementations of stfle, km and so on - // to detect cryptographic features. - if !haveAsmFunctions() { - return - } - - // optional cryptographic functions - if S390X.HasMSA { - aes := []function{aes128, aes192, aes256} - - // cipher message - km, kmc := kmQuery(), kmcQuery() - S390X.HasAES = km.Has(aes...) - S390X.HasAESCBC = kmc.Has(aes...) - if S390X.HasSTFLE { - facilities := stfle() - if facilities.Has(msa4) { - kmctr := kmctrQuery() - S390X.HasAESCTR = kmctr.Has(aes...) - } - if facilities.Has(msa8) { - kma := kmaQuery() - S390X.HasAESGCM = kma.Has(aes...) - } - } - - // compute message digest - kimd := kimdQuery() // intermediate (no padding) - klmd := klmdQuery() // last (padding) - S390X.HasSHA1 = kimd.Has(sha1) && klmd.Has(sha1) - S390X.HasSHA256 = kimd.Has(sha256) && klmd.Has(sha256) - S390X.HasSHA512 = kimd.Has(sha512) && klmd.Has(sha512) - S390X.HasGHASH = kimd.Has(ghash) // KLMD-GHASH does not exist - sha3 := []function{ - sha3_224, sha3_256, sha3_384, sha3_512, - shake128, shake256, - } - S390X.HasSHA3 = kimd.Has(sha3...) && klmd.Has(sha3...) - } -} diff --git a/internal/xcpu/cpu_s390x.s b/internal/xcpu/cpu_s390x.s deleted file mode 100644 index 1fb4b701..00000000 --- a/internal/xcpu/cpu_s390x.s +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2019 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. - -//go:build gc - -#include "textflag.h" - -// func stfle() facilityList -TEXT ·stfle(SB), NOSPLIT|NOFRAME, $0-32 - MOVD $ret+0(FP), R1 - MOVD $3, R0 // last doubleword index to store - XC $32, (R1), (R1) // clear 4 doublewords (32 bytes) - WORD $0xb2b01000 // store facility list extended (STFLE) - RET - -// func kmQuery() queryResult -TEXT ·kmQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KM-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92E0024 // cipher message (KM) - RET - -// func kmcQuery() queryResult -TEXT ·kmcQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMC-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92F0024 // cipher message with chaining (KMC) - RET - -// func kmctrQuery() queryResult -TEXT ·kmctrQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMCTR-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB92D4024 // cipher message with counter (KMCTR) - RET - -// func kmaQuery() queryResult -TEXT ·kmaQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KMA-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xb9296024 // cipher message with authentication (KMA) - RET - -// func kimdQuery() queryResult -TEXT ·kimdQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KIMD-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB93E0024 // compute intermediate message digest (KIMD) - RET - -// func klmdQuery() queryResult -TEXT ·klmdQuery(SB), NOSPLIT|NOFRAME, $0-16 - MOVD $0, R0 // set function code to 0 (KLMD-Query) - MOVD $ret+0(FP), R1 // address of 16-byte return value - WORD $0xB93F0024 // compute last message digest (KLMD) - RET diff --git a/internal/xcpu/cpu_wasm.go b/internal/xcpu/cpu_wasm.go deleted file mode 100644 index 230aaab4..00000000 --- a/internal/xcpu/cpu_wasm.go +++ /dev/null @@ -1,17 +0,0 @@ -// Copyright 2019 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. - -//go:build wasm - -package xcpu - -// We're compiling the cpu package for an unknown (software-abstracted) CPU. -// Make CacheLinePad an empty struct and hope that the usual struct alignment -// rules are good enough. - -const cacheLineSize = 0 - -func initOptions() {} - -func archInit() {} diff --git a/internal/xcpu/cpu_x86.go b/internal/xcpu/cpu_x86.go deleted file mode 100644 index d2f83468..00000000 --- a/internal/xcpu/cpu_x86.go +++ /dev/null @@ -1,151 +0,0 @@ -// Copyright 2018 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. - -//go:build 386 || amd64 || amd64p32 - -package xcpu - -import "runtime" - -const cacheLineSize = 64 - -func initOptions() { - options = []option{ - {Name: "adx", Feature: &X86.HasADX}, - {Name: "aes", Feature: &X86.HasAES}, - {Name: "avx", Feature: &X86.HasAVX}, - {Name: "avx2", Feature: &X86.HasAVX2}, - {Name: "avx512", Feature: &X86.HasAVX512}, - {Name: "avx512f", Feature: &X86.HasAVX512F}, - {Name: "avx512cd", Feature: &X86.HasAVX512CD}, - {Name: "avx512er", Feature: &X86.HasAVX512ER}, - {Name: "avx512pf", Feature: &X86.HasAVX512PF}, - {Name: "avx512vl", Feature: &X86.HasAVX512VL}, - {Name: "avx512bw", Feature: &X86.HasAVX512BW}, - {Name: "avx512dq", Feature: &X86.HasAVX512DQ}, - {Name: "avx512ifma", Feature: &X86.HasAVX512IFMA}, - {Name: "avx512vbmi", Feature: &X86.HasAVX512VBMI}, - {Name: "avx512vnniw", Feature: &X86.HasAVX5124VNNIW}, - {Name: "avx5124fmaps", Feature: &X86.HasAVX5124FMAPS}, - {Name: "avx512vpopcntdq", Feature: &X86.HasAVX512VPOPCNTDQ}, - {Name: "avx512vpclmulqdq", Feature: &X86.HasAVX512VPCLMULQDQ}, - {Name: "avx512vnni", Feature: &X86.HasAVX512VNNI}, - {Name: "avx512gfni", Feature: &X86.HasAVX512GFNI}, - {Name: "avx512vaes", Feature: &X86.HasAVX512VAES}, - {Name: "avx512vbmi2", Feature: &X86.HasAVX512VBMI2}, - {Name: "avx512bitalg", Feature: &X86.HasAVX512BITALG}, - {Name: "avx512bf16", Feature: &X86.HasAVX512BF16}, - {Name: "amxtile", Feature: &X86.HasAMXTile}, - {Name: "amxint8", Feature: &X86.HasAMXInt8}, - {Name: "amxbf16", Feature: &X86.HasAMXBF16}, - {Name: "bmi1", Feature: &X86.HasBMI1}, - {Name: "bmi2", Feature: &X86.HasBMI2}, - {Name: "cx16", Feature: &X86.HasCX16}, - {Name: "erms", Feature: &X86.HasERMS}, - {Name: "fma", Feature: &X86.HasFMA}, - {Name: "osxsave", Feature: &X86.HasOSXSAVE}, - {Name: "pclmulqdq", Feature: &X86.HasPCLMULQDQ}, - {Name: "popcnt", Feature: &X86.HasPOPCNT}, - {Name: "rdrand", Feature: &X86.HasRDRAND}, - {Name: "rdseed", Feature: &X86.HasRDSEED}, - {Name: "sse3", Feature: &X86.HasSSE3}, - {Name: "sse41", Feature: &X86.HasSSE41}, - {Name: "sse42", Feature: &X86.HasSSE42}, - {Name: "ssse3", Feature: &X86.HasSSSE3}, - - // These capabilities should always be enabled on amd64: - {Name: "sse2", Feature: &X86.HasSSE2, Required: runtime.GOARCH == "amd64"}, - } -} - -func archInit() { - - Initialized = true - - maxID, _, _, _ := cpuid(0, 0) - - if maxID < 1 { - return - } - - _, _, ecx1, edx1 := cpuid(1, 0) - X86.HasSSE2 = isSet(26, edx1) - - X86.HasSSE3 = isSet(0, ecx1) - X86.HasPCLMULQDQ = isSet(1, ecx1) - X86.HasSSSE3 = isSet(9, ecx1) - X86.HasFMA = isSet(12, ecx1) - X86.HasCX16 = isSet(13, ecx1) - X86.HasSSE41 = isSet(19, ecx1) - X86.HasSSE42 = isSet(20, ecx1) - X86.HasPOPCNT = isSet(23, ecx1) - X86.HasAES = isSet(25, ecx1) - X86.HasOSXSAVE = isSet(27, ecx1) - X86.HasRDRAND = isSet(30, ecx1) - - var osSupportsAVX, osSupportsAVX512 bool - // For XGETBV, OSXSAVE bit is required and sufficient. - if X86.HasOSXSAVE { - eax, _ := xgetbv() - // Check if XMM and YMM registers have OS support. - osSupportsAVX = isSet(1, eax) && isSet(2, eax) - - if runtime.GOOS == "darwin" { - // Darwin doesn't save/restore AVX-512 mask registers correctly across signal handlers. - // Since users can't rely on mask register contents, let's not advertise AVX-512 support. - // See issue 49233. - osSupportsAVX512 = false - } else { - // Check if OPMASK and ZMM registers have OS support. - osSupportsAVX512 = osSupportsAVX && isSet(5, eax) && isSet(6, eax) && isSet(7, eax) - } - } - - X86.HasAVX = isSet(28, ecx1) && osSupportsAVX - - if maxID < 7 { - return - } - - _, ebx7, ecx7, edx7 := cpuid(7, 0) - X86.HasBMI1 = isSet(3, ebx7) - X86.HasAVX2 = isSet(5, ebx7) && osSupportsAVX - X86.HasBMI2 = isSet(8, ebx7) - X86.HasERMS = isSet(9, ebx7) - X86.HasRDSEED = isSet(18, ebx7) - X86.HasADX = isSet(19, ebx7) - - X86.HasAVX512 = isSet(16, ebx7) && osSupportsAVX512 // Because avx-512 foundation is the core required extension - if X86.HasAVX512 { - X86.HasAVX512F = true - X86.HasAVX512CD = isSet(28, ebx7) - X86.HasAVX512ER = isSet(27, ebx7) - X86.HasAVX512PF = isSet(26, ebx7) - X86.HasAVX512VL = isSet(31, ebx7) - X86.HasAVX512BW = isSet(30, ebx7) - X86.HasAVX512DQ = isSet(17, ebx7) - X86.HasAVX512IFMA = isSet(21, ebx7) - X86.HasAVX512VBMI = isSet(1, ecx7) - X86.HasAVX5124VNNIW = isSet(2, edx7) - X86.HasAVX5124FMAPS = isSet(3, edx7) - X86.HasAVX512VPOPCNTDQ = isSet(14, ecx7) - X86.HasAVX512VPCLMULQDQ = isSet(10, ecx7) - X86.HasAVX512VNNI = isSet(11, ecx7) - X86.HasAVX512GFNI = isSet(8, ecx7) - X86.HasAVX512VAES = isSet(9, ecx7) - X86.HasAVX512VBMI2 = isSet(6, ecx7) - X86.HasAVX512BITALG = isSet(12, ecx7) - - eax71, _, _, _ := cpuid(7, 1) - X86.HasAVX512BF16 = isSet(5, eax71) - } - - X86.HasAMXTile = isSet(24, edx7) - X86.HasAMXInt8 = isSet(25, edx7) - X86.HasAMXBF16 = isSet(22, edx7) -} - -func isSet(bitpos uint, value uint32) bool { - return value&(1<> 63)) -) - -// For those platforms don't have a 'cpuid' equivalent we use HWCAP/HWCAP2 -// These are initialized in cpu_$GOARCH.go -// and should not be changed after they are initialized. -var hwCap uint -var hwCap2 uint - -func readHWCAP() error { - // For Go 1.21+, get auxv from the Go runtime. - if a := getAuxv(); len(a) > 0 { - for len(a) >= 2 { - tag, val := a[0], uint(a[1]) - a = a[2:] - switch tag { - case _AT_HWCAP: - hwCap = val - case _AT_HWCAP2: - hwCap2 = val - } - } - return nil - } - - buf, err := os.ReadFile(procAuxv) - if err != nil { - // e.g. on android /proc/self/auxv is not accessible, so silently - // ignore the error and leave Initialized = false. On some - // architectures (e.g. arm64) doinit() implements a fallback - // readout and will set Initialized = true again. - return err - } - bo := hostByteOrder() - for len(buf) >= 2*(uintSize/8) { - var tag, val uint - switch uintSize { - case 32: - tag = uint(bo.Uint32(buf[0:])) - val = uint(bo.Uint32(buf[4:])) - buf = buf[8:] - case 64: - tag = uint(bo.Uint64(buf[0:])) - val = uint(bo.Uint64(buf[8:])) - buf = buf[16:] - } - switch tag { - case _AT_HWCAP: - hwCap = val - case _AT_HWCAP2: - hwCap2 = val - } - } - return nil -} diff --git a/internal/xcpu/parse.go b/internal/xcpu/parse.go deleted file mode 100644 index be30b60f..00000000 --- a/internal/xcpu/parse.go +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2022 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 xcpu - -import "strconv" - -// parseRelease parses a dot-separated version number. It follows the semver -// syntax, but allows the minor and patch versions to be elided. -// -// This is a copy of the Go runtime's parseRelease from -// https://golang.org/cl/209597. -func parseRelease(rel string) (major, minor, patch int, ok bool) { - // Strip anything after a dash or plus. - for i := 0; i < len(rel); i++ { - if rel[i] == '-' || rel[i] == '+' { - rel = rel[:i] - break - } - } - - next := func() (int, bool) { - for i := 0; i < len(rel); i++ { - if rel[i] == '.' { - ver, err := strconv.Atoi(rel[:i]) - rel = rel[i+1:] - return ver, err == nil - } - } - ver, err := strconv.Atoi(rel) - rel = "" - return ver, err == nil - } - if major, ok = next(); !ok || rel == "" { - return - } - if minor, ok = next(); !ok || rel == "" { - return - } - patch, ok = next() - return -} diff --git a/internal/xcpu/proc_cpuinfo_linux.go b/internal/xcpu/proc_cpuinfo_linux.go deleted file mode 100644 index 9c88d24e..00000000 --- a/internal/xcpu/proc_cpuinfo_linux.go +++ /dev/null @@ -1,53 +0,0 @@ -// Copyright 2022 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. - -//go:build linux && arm64 - -package xcpu - -import ( - "errors" - "io" - "os" - "strings" -) - -func readLinuxProcCPUInfo() error { - f, err := os.Open("/proc/cpuinfo") - if err != nil { - return err - } - defer f.Close() - - var buf [1 << 10]byte // enough for first CPU - n, err := io.ReadFull(f, buf[:]) - if err != nil && err != io.ErrUnexpectedEOF { - return err - } - in := string(buf[:n]) - const features = "\nFeatures : " - i := strings.Index(in, features) - if i == -1 { - return errors.New("no CPU features found") - } - in = in[i+len(features):] - if i := strings.Index(in, "\n"); i != -1 { - in = in[:i] - } - m := map[string]*bool{} - - initOptions() // need it early here; it's harmless to call twice - for _, o := range options { - m[o.Name] = o.Feature - } - // The EVTSTRM field has alias "evstrm" in Go, but Linux calls it "evtstrm". - m["evtstrm"] = &ARM64.HasEVTSTRM - - for _, f := range strings.Fields(in) { - if p, ok := m[f]; ok { - *p = true - } - } - return nil -} diff --git a/internal/xcpu/runtime_auxv.go b/internal/xcpu/runtime_auxv.go deleted file mode 100644 index b842842e..00000000 --- a/internal/xcpu/runtime_auxv.go +++ /dev/null @@ -1,16 +0,0 @@ -// Copyright 2023 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 xcpu - -// getAuxvFn is non-nil on Go 1.21+ (via runtime_auxv_go121.go init) -// on platforms that use auxv. -var getAuxvFn func() []uintptr - -func getAuxv() []uintptr { - if getAuxvFn == nil { - return nil - } - return getAuxvFn() -} diff --git a/internal/xcpu/runtime_auxv_go121.go b/internal/xcpu/runtime_auxv_go121.go deleted file mode 100644 index b4dba06a..00000000 --- a/internal/xcpu/runtime_auxv_go121.go +++ /dev/null @@ -1,18 +0,0 @@ -// Copyright 2023 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. - -//go:build go1.21 - -package xcpu - -import ( - _ "unsafe" // for linkname -) - -//go:linkname runtime_getAuxv runtime.getAuxv -func runtime_getAuxv() []uintptr - -func init() { - getAuxvFn = runtime_getAuxv -} diff --git a/internal/xcpu/syscall_aix_gccgo.go b/internal/xcpu/syscall_aix_gccgo.go deleted file mode 100644 index 905566fe..00000000 --- a/internal/xcpu/syscall_aix_gccgo.go +++ /dev/null @@ -1,26 +0,0 @@ -// 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. - -// Recreate a getsystemcfg syscall handler instead of -// using the one provided by x/sys/unix to avoid having -// the dependency between them. (See golang.org/issue/32102) -// Moreover, this file will be used during the building of -// gccgo's libgo and thus must not used a CGo method. - -//go:build aix && gccgo - -package xcpu - -import ( - "syscall" -) - -//extern getsystemcfg -func gccgoGetsystemcfg(label uint32) (r uint64) - -func callgetsystemcfg(label int) (r1 uintptr, e1 syscall.Errno) { - r1 = uintptr(gccgoGetsystemcfg(uint32(label))) - e1 = syscall.GetErrno() - return -} diff --git a/internal/xcpu/syscall_aix_ppc64_gc.go b/internal/xcpu/syscall_aix_ppc64_gc.go deleted file mode 100644 index 18837396..00000000 --- a/internal/xcpu/syscall_aix_ppc64_gc.go +++ /dev/null @@ -1,35 +0,0 @@ -// Copyright 2019 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. - -// Minimal copy of x/sys/unix so the cpu package can make a -// system call on AIX without depending on x/sys/unix. -// (See golang.org/issue/32102) - -//go:build aix && ppc64 && gc - -package xcpu - -import ( - "syscall" - "unsafe" -) - -//go:cgo_import_dynamic libc_getsystemcfg getsystemcfg "libc.a/shr_64.o" - -//go:linkname libc_getsystemcfg libc_getsystemcfg - -type syscallFunc uintptr - -var libc_getsystemcfg syscallFunc - -type errno = syscall.Errno - -// Implemented in runtime/syscall_aix.go. -func rawSyscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err errno) -func syscall6(trap, nargs, a1, a2, a3, a4, a5, a6 uintptr) (r1, r2 uintptr, err errno) - -func callgetsystemcfg(label int) (r1 uintptr, e1 errno) { - r1, _, e1 = syscall6(uintptr(unsafe.Pointer(&libc_getsystemcfg)), 1, uintptr(label), 0, 0, 0, 0, 0) - return -} diff --git a/mask_amd64.s b/mask_amd64.s index caca53ec..bd42be31 100644 --- a/mask_amd64.s +++ b/mask_amd64.s @@ -26,11 +26,6 @@ TEXT ·maskAsm(SB), NOSPLIT, $0-28 TESTQ $31, AX JNZ unaligned -aligned: - CMPB ·useAVX2(SB), $1 - JE avx2 - JMP sse - unaligned_loop_1byte: XORB SI, (AX) INCQ AX @@ -47,7 +42,7 @@ unaligned_loop_1byte: ORQ DX, DI TESTQ $31, AX - JZ aligned + JZ sse unaligned: TESTQ $7, AX // AND $7 & len, if not zero jump to loop_1b. @@ -60,27 +55,7 @@ unaligned_loop: SUBQ $8, CX TESTQ $31, AX JNZ unaligned_loop - JMP aligned - -avx2: - CMPQ CX, $0x80 - JL sse - VMOVQ DI, X0 - VPBROADCASTQ X0, Y0 - -avx2_loop: - VPXOR (AX), Y0, Y1 - VPXOR 32(AX), Y0, Y2 - VPXOR 64(AX), Y0, Y3 - VPXOR 96(AX), Y0, Y4 - VMOVDQU Y1, (AX) - VMOVDQU Y2, 32(AX) - VMOVDQU Y3, 64(AX) - VMOVDQU Y4, 96(AX) - ADDQ $0x80, AX - SUBQ $0x80, CX - CMPQ CX, $0x80 - JAE avx2_loop // loop if CX >= 0x80 + JMP sse sse: CMPQ CX, $0x40 diff --git a/mask_asm.go b/mask_asm.go index 3b1ee517..259eec03 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -2,8 +2,6 @@ package websocket -import "nhooyr.io/websocket/internal/xcpu" - func mask(b []byte, key uint32) uint32 { if len(b) > 0 { return maskAsm(&b[0], len(b), key) @@ -11,14 +9,13 @@ func mask(b []byte, key uint32) uint32 { return key } -//lint:ignore U1000 mask_*.s -var useAVX2 = xcpu.X86.HasAVX2 - // @nhooyr: I am not confident that the amd64 or the arm64 implementations of this // function are perfect. There are almost certainly missing optimizations or -// opportunities for // simplification. I'm confident there are no bugs though. +// opportunities for simplification. I'm confident there are no bugs though. // For example, the arm64 implementation doesn't align memory like the amd64. // Or the amd64 implementation could use AVX512 instead of just AVX2. +// The AVX2 code I had to disable anyway as it wasn't performing as expected. +// See https://github.com/nhooyr/websocket/pull/326#issuecomment-1771138049 // //go:noescape func maskAsm(b *byte, len int, key uint32) uint32 From 2cd18b3742d1b29df86bd8daa81fc55fe26f9f8c Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 22 Feb 2024 05:39:37 -0800 Subject: [PATCH 139/152] README.md: Link to assembly benchmark results --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3dead855..cde3ec6d 100644 --- a/README.md +++ b/README.md @@ -120,7 +120,7 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [3-4x](https://github.com/nhooyr/websocket/pull/326) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go +- [3.5x](https://github.com/nhooyr/websocket/pull/326#issuecomment-1959470758) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode From 4c273310109b8d134d96b35d66d7231e8c54a05b Mon Sep 17 00:00:00 2001 From: Grigorii Khvatskii Date: Mon, 26 Feb 2024 16:12:18 -0500 Subject: [PATCH 140/152] Fix unaligned load error on 32-bit architectures On some 32-bit architectures, 64-bit atomic operations panic when the value is not aligned properly. In this package, this causes netConn operations to panic when compiling with GOARCH=386, since netConn does atomic operations with int64 values in the netConn struct (namely, with readExpired and writeExpired). This commit fixes this by moving readExpired and writeExpired to the beginning of the struct, which makes them properly aligned. --- netconn.go | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/netconn.go b/netconn.go index 1667f45c..133ba55f 100644 --- a/netconn.go +++ b/netconn.go @@ -94,22 +94,23 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { } type netConn struct { + readExpired int64 + writeExpired int64 + c *Conn msgType MessageType - writeTimer *time.Timer - writeMu *mu - writeExpired int64 - writeCtx context.Context - writeCancel context.CancelFunc - - readTimer *time.Timer - readMu *mu - readExpired int64 - readCtx context.Context - readCancel context.CancelFunc - readEOFed bool - reader io.Reader + writeTimer *time.Timer + writeMu *mu + writeCtx context.Context + writeCancel context.CancelFunc + + readTimer *time.Timer + readMu *mu + readCtx context.Context + readCancel context.CancelFunc + readEOFed bool + reader io.Reader } var _ net.Conn = &netConn{} From 819f9d18929d8db111767c09d0c35c5c6cceb9c8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Tue, 5 Mar 2024 14:11:05 -0800 Subject: [PATCH 141/152] close.go: Fix comment fmt --- close.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/close.go b/close.go index c3dee7e0..e925d043 100644 --- a/close.go +++ b/close.go @@ -93,8 +93,7 @@ func CloseStatus(err error) StatusCode { // The connection can only be closed once. Additional calls to Close // are no-ops. // -// The maximum length of reason must be 125 bytes. Avoid -// sending a dynamic reason. +// The maximum length of reason must be 125 bytes. Avoid sending a dynamic reason. // // Close will unblock all goroutines interacting with the connection once // complete. From 1cc90bb49096127bb51ee1bfe860bdee099e94d8 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Thu, 7 Mar 2024 11:39:07 -0800 Subject: [PATCH 142/152] netconn: Explain why we start with the two int64 atomics --- netconn.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/netconn.go b/netconn.go index 133ba55f..3324014d 100644 --- a/netconn.go +++ b/netconn.go @@ -94,6 +94,8 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { } type netConn struct { + // These must be first to be aligned on 32 bit platforms. + // https://github.com/nhooyr/websocket/pull/438 readExpired int64 writeExpired int64 From 46f41124ad13951bf574ad485b2db964d0172434 Mon Sep 17 00:00:00 2001 From: Alexander Wang Date: Mon, 18 Dec 2023 15:32:54 -0800 Subject: [PATCH 143/152] fix closenow race --- accept_test.go | 39 +++++++++++++++++++++++++++++++++++++++ close.go | 2 ++ 2 files changed, 41 insertions(+) diff --git a/accept_test.go b/accept_test.go index 7cb85d0f..9ab0ddf5 100644 --- a/accept_test.go +++ b/accept_test.go @@ -6,10 +6,12 @@ package websocket import ( "bufio" "errors" + "io" "net" "net/http" "net/http/httptest" "strings" + "sync" "testing" "nhooyr.io/websocket/internal/test/assert" @@ -142,6 +144,43 @@ func TestAccept(t *testing.T) { _, err := Accept(w, r, nil) assert.Contains(t, err, `failed to hijack connection`) }) + t.Run("closeRace", func(t *testing.T) { + t.Parallel() + + server, _ := net.Pipe() + + pr, pw := io.Pipe() + rw := bufio.NewReadWriter(bufio.NewReader(pr), bufio.NewWriter(pw)) + newResponseWriter := func() http.ResponseWriter { + return mockHijacker{ + ResponseWriter: httptest.NewRecorder(), + hijack: func() (net.Conn, *bufio.ReadWriter, error) { + return server, rw, nil + }, + } + } + w := newResponseWriter() + + r := httptest.NewRequest("GET", "/", nil) + r.Header.Set("Connection", "Upgrade") + r.Header.Set("Upgrade", "websocket") + r.Header.Set("Sec-WebSocket-Version", "13") + r.Header.Set("Sec-WebSocket-Key", xrand.Base64(16)) + + c, err := Accept(w, r, nil) + wg := &sync.WaitGroup{} + wg.Add(2) + go func() { + c.Close(StatusInternalError, "the sky is falling") + wg.Done() + }() + go func() { + c.CloseNow() + wg.Done() + }() + wg.Wait() + assert.Success(t, err) + }) } func Test_verifyClientHandshake(t *testing.T) { diff --git a/close.go b/close.go index e925d043..21edcf11 100644 --- a/close.go +++ b/close.go @@ -113,6 +113,8 @@ func (c *Conn) CloseNow() (err error) { } c.close(nil) + c.closeMu.Lock() + defer c.closeMu.Unlock() return c.closeErr } From 0b3912f68dc3389749e822b03bbbb8e7a138fd11 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 16:23:59 -0700 Subject: [PATCH 144/152] accept_test: Fix @alixander's test Not ideal but whatever, I'm going to rewrite all of this anyway. --- accept_test.go | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/accept_test.go b/accept_test.go index 9ab0ddf5..18233b1e 100644 --- a/accept_test.go +++ b/accept_test.go @@ -6,7 +6,6 @@ package websocket import ( "bufio" "errors" - "io" "net" "net/http" "net/http/httptest" @@ -149,8 +148,7 @@ func TestAccept(t *testing.T) { server, _ := net.Pipe() - pr, pw := io.Pipe() - rw := bufio.NewReadWriter(bufio.NewReader(pr), bufio.NewWriter(pw)) + rw := bufio.NewReadWriter(bufio.NewReader(server), bufio.NewWriter(server)) newResponseWriter := func() http.ResponseWriter { return mockHijacker{ ResponseWriter: httptest.NewRecorder(), From db18a31624813282beba3c7e0219dd2f2f1c522d Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 16:24:20 -0700 Subject: [PATCH 145/152] close.go: Rewrite how the library handles closing Far simpler now. Sorry this took a while. Closes #427 Closes #429 Closes #434 Closes #436 Closes #437 --- close.go | 154 ++++++++++++++++++++++++++++++++++----------------- conn.go | 74 ++++++++++--------------- conn_test.go | 3 + read.go | 29 +++++----- write.go | 24 +------- ws_js.go | 2 - 6 files changed, 150 insertions(+), 136 deletions(-) diff --git a/close.go b/close.go index 21edcf11..820625ac 100644 --- a/close.go +++ b/close.go @@ -97,82 +97,106 @@ func CloseStatus(err error) StatusCode { // // Close will unblock all goroutines interacting with the connection once // complete. -func (c *Conn) Close(code StatusCode, reason string) error { - defer c.wg.Wait() - return c.closeHandshake(code, reason) +func (c *Conn) Close(code StatusCode, reason string) (err error) { + defer errd.Wrap(&err, "failed to close WebSocket") + + if !c.casClosing() { + err = c.waitGoroutines() + if err != nil { + return err + } + return net.ErrClosed + } + defer func() { + if errors.Is(err, net.ErrClosed) { + err = nil + } + }() + + err = c.closeHandshake(code, reason) + + err2 := c.close() + if err == nil && err2 != nil { + err = err2 + } + + err2 = c.waitGoroutines() + if err == nil && err2 != nil { + err = err2 + } + + return err } // CloseNow closes the WebSocket connection without attempting a close handshake. // Use when you do not want the overhead of the close handshake. func (c *Conn) CloseNow() (err error) { - defer c.wg.Wait() defer errd.Wrap(&err, "failed to close WebSocket") - if c.isClosed() { + if !c.casClosing() { + err = c.waitGoroutines() + if err != nil { + return err + } return net.ErrClosed } + defer func() { + if errors.Is(err, net.ErrClosed) { + err = nil + } + }() - c.close(nil) - c.closeMu.Lock() - defer c.closeMu.Unlock() - return c.closeErr -} - -func (c *Conn) closeHandshake(code StatusCode, reason string) (err error) { - defer errd.Wrap(&err, "failed to close WebSocket") - - writeErr := c.writeClose(code, reason) - closeHandshakeErr := c.waitCloseHandshake() + err = c.close() - if writeErr != nil { - return writeErr + err2 := c.waitGoroutines() + if err == nil && err2 != nil { + err = err2 } + return err +} - if CloseStatus(closeHandshakeErr) == -1 && !errors.Is(net.ErrClosed, closeHandshakeErr) { - return closeHandshakeErr +func (c *Conn) closeHandshake(code StatusCode, reason string) error { + err := c.writeClose(code, reason) + if err != nil { + return err } + err = c.waitCloseHandshake() + if CloseStatus(err) != code { + return err + } return nil } func (c *Conn) writeClose(code StatusCode, reason string) error { - c.closeMu.Lock() - wroteClose := c.wroteClose - c.wroteClose = true - c.closeMu.Unlock() - if wroteClose { - return net.ErrClosed - } - ce := CloseError{ Code: code, Reason: reason, } var p []byte - var marshalErr error + var err error if ce.Code != StatusNoStatusRcvd { - p, marshalErr = ce.bytes() - } - - writeErr := c.writeControl(context.Background(), opClose, p) - if CloseStatus(writeErr) != -1 { - // Not a real error if it's due to a close frame being received. - writeErr = nil + p, err = ce.bytes() + if err != nil { + return err + } } - // We do this after in case there was an error writing the close frame. - c.setCloseErr(fmt.Errorf("sent close frame: %w", ce)) + ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) + defer cancel() - if marshalErr != nil { - return marshalErr + err = c.writeControl(ctx, opClose, p) + // If the connection closed as we're writing we ignore the error as we might + // have written the close frame, the peer responded and then someone else read it + // and closed the connection. + if err != nil && !errors.Is(err, net.ErrClosed) { + return err } - return writeErr + return nil } func (c *Conn) waitCloseHandshake() error { - defer c.close(nil) - ctx, cancel := context.WithTimeout(context.Background(), time.Second*5) defer cancel() @@ -208,6 +232,36 @@ func (c *Conn) waitCloseHandshake() error { } } +func (c *Conn) waitGoroutines() error { + t := time.NewTimer(time.Second * 15) + defer t.Stop() + + select { + case <-c.timeoutLoopDone: + case <-t.C: + return errors.New("failed to wait for timeoutLoop goroutine to exit") + } + + c.closeReadMu.Lock() + ctx := c.closeReadCtx + c.closeReadMu.Unlock() + if ctx != nil { + select { + case <-ctx.Done(): + case <-t.C: + return errors.New("failed to wait for close read goroutine to exit") + } + } + + select { + case <-c.closed: + case <-t.C: + return errors.New("failed to wait for connection to be closed") + } + + return nil +} + func parseClosePayload(p []byte) (CloseError, error) { if len(p) == 0 { return CloseError{ @@ -278,16 +332,14 @@ func (ce CloseError) bytesErr() ([]byte, error) { return buf, nil } -func (c *Conn) setCloseErr(err error) { +func (c *Conn) casClosing() bool { c.closeMu.Lock() - c.setCloseErrLocked(err) - c.closeMu.Unlock() -} - -func (c *Conn) setCloseErrLocked(err error) { - if c.closeErr == nil && err != nil { - c.closeErr = fmt.Errorf("WebSocket closed: %w", err) + defer c.closeMu.Unlock() + if !c.closing { + c.closing = true + return true } + return false } func (c *Conn) isClosed() bool { diff --git a/conn.go b/conn.go index ef4d62ad..8ba82962 100644 --- a/conn.go +++ b/conn.go @@ -6,7 +6,6 @@ package websocket import ( "bufio" "context" - "errors" "fmt" "io" "net" @@ -53,8 +52,9 @@ type Conn struct { br *bufio.Reader bw *bufio.Writer - readTimeout chan context.Context - writeTimeout chan context.Context + readTimeout chan context.Context + writeTimeout chan context.Context + timeoutLoopDone chan struct{} // Read state. readMu *mu @@ -70,11 +70,12 @@ type Conn struct { writeHeaderBuf [8]byte writeHeader header - wg sync.WaitGroup - closed chan struct{} - closeMu sync.Mutex - closeErr error - wroteClose bool + closeReadMu sync.Mutex + closeReadCtx context.Context + + closed chan struct{} + closeMu sync.Mutex + closing bool pingCounter int32 activePingsMu sync.Mutex @@ -103,8 +104,9 @@ func newConn(cfg connConfig) *Conn { br: cfg.br, bw: cfg.bw, - readTimeout: make(chan context.Context), - writeTimeout: make(chan context.Context), + readTimeout: make(chan context.Context), + writeTimeout: make(chan context.Context), + timeoutLoopDone: make(chan struct{}), closed: make(chan struct{}), activePings: make(map[string]chan<- struct{}), @@ -128,14 +130,10 @@ func newConn(cfg connConfig) *Conn { } runtime.SetFinalizer(c, func(c *Conn) { - c.close(errors.New("connection garbage collected")) + c.close() }) - c.wg.Add(1) - go func() { - defer c.wg.Done() - c.timeoutLoop() - }() + go c.timeoutLoop() return c } @@ -146,35 +144,29 @@ func (c *Conn) Subprotocol() string { return c.subprotocol } -func (c *Conn) close(err error) { +func (c *Conn) close() error { c.closeMu.Lock() defer c.closeMu.Unlock() if c.isClosed() { - return - } - if err == nil { - err = c.rwc.Close() + return net.ErrClosed } - c.setCloseErrLocked(err) - - close(c.closed) runtime.SetFinalizer(c, nil) + close(c.closed) // Have to close after c.closed is closed to ensure any goroutine that wakes up // from the connection being closed also sees that c.closed is closed and returns // closeErr. - c.rwc.Close() - - c.wg.Add(1) - go func() { - defer c.wg.Done() - c.msgWriter.close() - c.msgReader.close() - }() + err := c.rwc.Close() + // With the close of rwc, these become safe to close. + c.msgWriter.close() + c.msgReader.close() + return err } func (c *Conn) timeoutLoop() { + defer close(c.timeoutLoopDone) + readCtx := context.Background() writeCtx := context.Background() @@ -187,14 +179,10 @@ func (c *Conn) timeoutLoop() { case readCtx = <-c.readTimeout: case <-readCtx.Done(): - c.setCloseErr(fmt.Errorf("read timed out: %w", readCtx.Err())) - c.wg.Add(1) - go func() { - defer c.wg.Done() - c.writeError(StatusPolicyViolation, errors.New("read timed out")) - }() + c.close() + return case <-writeCtx.Done(): - c.close(fmt.Errorf("write timed out: %w", writeCtx.Err())) + c.close() return } } @@ -243,9 +231,7 @@ func (c *Conn) ping(ctx context.Context, p string) error { case <-c.closed: return net.ErrClosed case <-ctx.Done(): - err := fmt.Errorf("failed to wait for pong: %w", ctx.Err()) - c.close(err) - return err + return fmt.Errorf("failed to wait for pong: %w", ctx.Err()) case <-pong: return nil } @@ -281,9 +267,7 @@ func (m *mu) lock(ctx context.Context) error { case <-m.c.closed: return net.ErrClosed case <-ctx.Done(): - err := fmt.Errorf("failed to acquire lock: %w", ctx.Err()) - m.c.close(err) - return err + return fmt.Errorf("failed to acquire lock: %w", ctx.Err()) case m.ch <- struct{}{}: // To make sure the connection is certainly alive. // As it's possible the send on m.ch was selected diff --git a/conn_test.go b/conn_test.go index 97b172dc..ff7279f5 100644 --- a/conn_test.go +++ b/conn_test.go @@ -345,6 +345,9 @@ func TestConn(t *testing.T) { func TestWasm(t *testing.T) { t.Parallel() + if os.Getenv("CI") == "" { + t.Skip() + } s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := echoServer(w, r, &websocket.AcceptOptions{ diff --git a/read.go b/read.go index 81b89831..bd0ddf95 100644 --- a/read.go +++ b/read.go @@ -60,14 +60,21 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { // Call CloseRead when you do not expect to read any more messages. // Since it actively reads from the connection, it will ensure that ping, pong and close // frames are responded to. This means c.Ping and c.Close will still work as expected. +// +// This function is idempotent. func (c *Conn) CloseRead(ctx context.Context) context.Context { + c.closeReadMu.Lock() + if c.closeReadCtx != nil { + c.closeReadMu.Unlock() + return c.closeReadCtx + } ctx, cancel := context.WithCancel(ctx) + c.closeReadCtx = ctx + c.closeReadMu.Unlock() - c.wg.Add(1) go func() { - defer c.CloseNow() - defer c.wg.Done() defer cancel() + defer c.close() _, _, err := c.Reader(ctx) if err == nil { c.Close(StatusPolicyViolation, "unexpected data message") @@ -222,7 +229,6 @@ func (c *Conn) readFrameHeader(ctx context.Context) (header, error) { case <-ctx.Done(): return header{}, ctx.Err() default: - c.close(err) return header{}, err } } @@ -251,9 +257,7 @@ func (c *Conn) readFramePayload(ctx context.Context, p []byte) (int, error) { case <-ctx.Done(): return n, ctx.Err() default: - err = fmt.Errorf("failed to read frame payload: %w", err) - c.close(err) - return n, err + return n, fmt.Errorf("failed to read frame payload: %w", err) } } @@ -320,9 +324,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { } err = fmt.Errorf("received close frame: %w", ce) - c.setCloseErr(err) c.writeClose(ce.Code, ce.Reason) - c.close(err) return err } @@ -336,9 +338,7 @@ func (c *Conn) reader(ctx context.Context) (_ MessageType, _ io.Reader, err erro defer c.readMu.unlock() if !c.msgReader.fin { - err = errors.New("previous message not read to completion") - c.close(fmt.Errorf("failed to get reader: %w", err)) - return 0, nil, err + return 0, nil, errors.New("previous message not read to completion") } h, err := c.readLoop(ctx) @@ -411,10 +411,9 @@ func (mr *msgReader) Read(p []byte) (n int, err error) { return n, io.EOF } if err != nil { - err = fmt.Errorf("failed to read: %w", err) - mr.c.close(err) + return n, fmt.Errorf("failed to read: %w", err) } - return n, err + return n, nil } func (mr *msgReader) read(p []byte) (int, error) { diff --git a/write.go b/write.go index 7ac7ce63..d7222f2d 100644 --- a/write.go +++ b/write.go @@ -159,7 +159,6 @@ func (mw *msgWriter) Write(p []byte) (_ int, err error) { defer func() { if err != nil { err = fmt.Errorf("failed to write: %w", err) - mw.c.close(err) } }() @@ -242,30 +241,12 @@ func (c *Conn) writeControl(ctx context.Context, opcode opcode, p []byte) error return nil } -// frame handles all writes to the connection. +// writeFrame handles all writes to the connection. func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opcode, p []byte) (_ int, err error) { err = c.writeFrameMu.lock(ctx) if err != nil { return 0, err } - - // If the state says a close has already been written, we wait until - // the connection is closed and return that error. - // - // However, if the frame being written is a close, that means its the close from - // the state being set so we let it go through. - c.closeMu.Lock() - wroteClose := c.wroteClose - c.closeMu.Unlock() - if wroteClose && opcode != opClose { - c.writeFrameMu.unlock() - select { - case <-ctx.Done(): - return 0, ctx.Err() - case <-c.closed: - return 0, net.ErrClosed - } - } defer c.writeFrameMu.unlock() select { @@ -283,7 +264,6 @@ func (c *Conn) writeFrame(ctx context.Context, fin bool, flate bool, opcode opco err = ctx.Err() default: } - c.close(err) err = fmt.Errorf("failed to write frame: %w", err) } }() @@ -392,7 +372,5 @@ func extractBufioWriterBuf(bw *bufio.Writer, w io.Writer) []byte { } func (c *Conn) writeError(code StatusCode, err error) { - c.setCloseErr(err) c.writeClose(code, err.Error()) - c.close(nil) } diff --git a/ws_js.go b/ws_js.go index 77d0d80f..2b8e3b3d 100644 --- a/ws_js.go +++ b/ws_js.go @@ -225,7 +225,6 @@ func (c *Conn) write(ctx context.Context, typ MessageType, p []byte) error { // or the connection is closed. // It thus performs the full WebSocket close handshake. func (c *Conn) Close(code StatusCode, reason string) error { - defer c.wg.Wait() err := c.exportedClose(code, reason) if err != nil { return fmt.Errorf("failed to close WebSocket: %w", err) @@ -239,7 +238,6 @@ func (c *Conn) Close(code StatusCode, reason string) error { // note: No different from Close(StatusGoingAway, "") in WASM as there is no way to close // a WebSocket without the close handshake. func (c *Conn) CloseNow() error { - defer c.wg.Wait() return c.Close(StatusGoingAway, "") } From 856e371494bab94984371763f53f114b4cef3547 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 16:39:03 -0700 Subject: [PATCH 146/152] ws_js: Update to match new close code --- read.go | 5 +++-- ws_js.go | 25 +++++++++++++++++-------- 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/read.go b/read.go index bd0ddf95..8bd73695 100644 --- a/read.go +++ b/read.go @@ -64,9 +64,10 @@ func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { // This function is idempotent. func (c *Conn) CloseRead(ctx context.Context) context.Context { c.closeReadMu.Lock() - if c.closeReadCtx != nil { + ctx2 := c.closeReadCtx + if ctx2 != nil { c.closeReadMu.Unlock() - return c.closeReadCtx + return ctx2 } ctx, cancel := context.WithCancel(ctx) c.closeReadCtx = ctx diff --git a/ws_js.go b/ws_js.go index 2b8e3b3d..02d61f28 100644 --- a/ws_js.go +++ b/ws_js.go @@ -47,9 +47,10 @@ type Conn struct { // read limit for a message in bytes. msgReadLimit xsync.Int64 - wg sync.WaitGroup + closeReadMu sync.Mutex + closeReadCtx context.Context + closingMu sync.Mutex - isReadClosed xsync.Int64 closeOnce sync.Once closed chan struct{} closeErrOnce sync.Once @@ -130,7 +131,10 @@ func (c *Conn) closeWithInternal() { // Read attempts to read a message from the connection. // The maximum time spent waiting is bounded by the context. func (c *Conn) Read(ctx context.Context) (MessageType, []byte, error) { - if c.isReadClosed.Load() == 1 { + c.closeReadMu.Lock() + closedRead := c.closeReadCtx != nil + c.closeReadMu.Unlock() + if closedRead { return 0, nil, errors.New("WebSocket connection read closed") } @@ -387,14 +391,19 @@ func (w *writer) Close() error { // CloseRead implements *Conn.CloseRead for wasm. func (c *Conn) CloseRead(ctx context.Context) context.Context { - c.isReadClosed.Store(1) - + c.closeReadMu.Lock() + ctx2 := c.closeReadCtx + if ctx2 != nil { + c.closeReadMu.Unlock() + return ctx2 + } ctx, cancel := context.WithCancel(ctx) - c.wg.Add(1) + c.closeReadCtx = ctx + c.closeReadMu.Unlock() + go func() { - defer c.CloseNow() - defer c.wg.Done() defer cancel() + defer c.CloseNow() _, _, err := c.read(ctx) if err != nil { c.Close(StatusPolicyViolation, "unexpected data message") From 0edbb2805cd3da973ff35ab5b54969a38f6eaecf Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 16:44:17 -0700 Subject: [PATCH 147/152] netconn: fmt --- netconn.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/netconn.go b/netconn.go index 3324014d..86f7dadb 100644 --- a/netconn.go +++ b/netconn.go @@ -94,7 +94,7 @@ func NetConn(ctx context.Context, c *Conn, msgType MessageType) net.Conn { } type netConn struct { - // These must be first to be aligned on 32 bit platforms. + // These must be first to be aligned on 32 bit platforms. // https://github.com/nhooyr/websocket/pull/438 readExpired int64 writeExpired int64 From c97fb09c4f6ddbecf13cbfe6564367d05006d007 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Fri, 5 Apr 2024 17:45:54 -0700 Subject: [PATCH 148/152] mask_asm: Disable for now until v1.9.0 See #326 --- mask.go | 2 -- mask_asm.go | 13 +++++++++---- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/mask.go b/mask.go index 5f0746dc..7bc0c8d5 100644 --- a/mask.go +++ b/mask.go @@ -16,8 +16,6 @@ import ( // to be in little endian. // // See https://github.com/golang/go/issues/31586 -// -//lint:ignore U1000 mask.go func maskGo(b []byte, key uint32) uint32 { if len(b) >= 8 { key64 := uint64(key)<<32 | uint64(key) diff --git a/mask_asm.go b/mask_asm.go index 259eec03..60c0290f 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -3,10 +3,14 @@ package websocket func mask(b []byte, key uint32) uint32 { - if len(b) > 0 { - return maskAsm(&b[0], len(b), key) - } - return key + // TODO: Will enable in v1.9.0. + return maskGo(b, key) + /* + if len(b) > 0 { + return maskAsm(&b[0], len(b), key) + } + return key + */ } // @nhooyr: I am not confident that the amd64 or the arm64 implementations of this @@ -18,4 +22,5 @@ func mask(b []byte, key uint32) uint32 { // See https://github.com/nhooyr/websocket/pull/326#issuecomment-1771138049 // //go:noescape +//lint:ignore U1000 disabled till v1.9.0 func maskAsm(b *byte, len int, key uint32) uint32 From 211ef4bcce3f6cf639c870a00d464a2676416066 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 7 Apr 2024 07:39:48 -0700 Subject: [PATCH 149/152] ws_js_test: Fix --- close.go | 4 ---- conn.go | 1 - conn_test.go | 2 +- read.go | 6 +++--- 4 files changed, 4 insertions(+), 9 deletions(-) diff --git a/close.go b/close.go index 820625ac..d1512597 100644 --- a/close.go +++ b/close.go @@ -206,10 +206,6 @@ func (c *Conn) waitCloseHandshake() error { } defer c.readMu.unlock() - if c.readCloseFrameErr != nil { - return c.readCloseFrameErr - } - for i := int64(0); i < c.msgReader.payloadLength; i++ { _, err := c.br.ReadByte() if err != nil { diff --git a/conn.go b/conn.go index 8ba82962..f5da573a 100644 --- a/conn.go +++ b/conn.go @@ -61,7 +61,6 @@ type Conn struct { readHeaderBuf [8]byte readControlBuf [maxControlPayload]byte msgReader *msgReader - readCloseFrameErr error // Write state. msgWriter *msgWriter diff --git a/conn_test.go b/conn_test.go index ff7279f5..9fbe961d 100644 --- a/conn_test.go +++ b/conn_test.go @@ -363,7 +363,7 @@ func TestWasm(t *testing.T) { ctx, cancel := context.WithTimeout(context.Background(), time.Minute) defer cancel() - cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".") + cmd := exec.CommandContext(ctx, "go", "test", "-exec=wasmbrowsertest", ".", "-v") cmd.Env = append(os.Environ(), "GOOS=js", "GOARCH=wasm", fmt.Sprintf("WS_ECHO_SERVER_URL=%v", s.URL)) b, err := cmd.CombinedOutput() diff --git a/read.go b/read.go index 8bd73695..5df031ca 100644 --- a/read.go +++ b/read.go @@ -313,9 +313,7 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { return nil } - defer func() { - c.readCloseFrameErr = err - }() + // opClose ce, err := parseClosePayload(b) if err != nil { @@ -326,6 +324,8 @@ func (c *Conn) handleControl(ctx context.Context, h header) (err error) { err = fmt.Errorf("received close frame: %w", ce) c.writeClose(ce.Code, ce.Reason) + c.readMu.unlock() + c.close() return err } From 250db1efbe15806649120e4f6748de43859b5d12 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 7 Apr 2024 07:47:26 -0700 Subject: [PATCH 150/152] read: Fix CloseRead to have its own done channel Context can be cancelled by parent. Doesn't indicate the CloseRead goroutine has exited. --- close.go | 6 +++--- conn.go | 13 +++++++------ mask_asm.go | 2 +- read.go | 2 ++ 4 files changed, 13 insertions(+), 10 deletions(-) diff --git a/close.go b/close.go index d1512597..625ed121 100644 --- a/close.go +++ b/close.go @@ -239,11 +239,11 @@ func (c *Conn) waitGoroutines() error { } c.closeReadMu.Lock() - ctx := c.closeReadCtx + closeRead := c.closeReadCtx != nil c.closeReadMu.Unlock() - if ctx != nil { + if closeRead { select { - case <-ctx.Done(): + case <-c.closeReadDone: case <-t.C: return errors.New("failed to wait for close read goroutine to exit") } diff --git a/conn.go b/conn.go index f5da573a..8690fb3b 100644 --- a/conn.go +++ b/conn.go @@ -57,10 +57,10 @@ type Conn struct { timeoutLoopDone chan struct{} // Read state. - readMu *mu - readHeaderBuf [8]byte - readControlBuf [maxControlPayload]byte - msgReader *msgReader + readMu *mu + readHeaderBuf [8]byte + readControlBuf [maxControlPayload]byte + msgReader *msgReader // Write state. msgWriter *msgWriter @@ -69,8 +69,9 @@ type Conn struct { writeHeaderBuf [8]byte writeHeader header - closeReadMu sync.Mutex - closeReadCtx context.Context + closeReadMu sync.Mutex + closeReadCtx context.Context + closeReadDone chan struct{} closed chan struct{} closeMu sync.Mutex diff --git a/mask_asm.go b/mask_asm.go index 60c0290f..f9484b5b 100644 --- a/mask_asm.go +++ b/mask_asm.go @@ -3,7 +3,7 @@ package websocket func mask(b []byte, key uint32) uint32 { - // TODO: Will enable in v1.9.0. + // TODO: Will enable in v1.9.0. return maskGo(b, key) /* if len(b) > 0 { diff --git a/read.go b/read.go index 5df031ca..a59e71d9 100644 --- a/read.go +++ b/read.go @@ -71,9 +71,11 @@ func (c *Conn) CloseRead(ctx context.Context) context.Context { } ctx, cancel := context.WithCancel(ctx) c.closeReadCtx = ctx + c.closeReadDone = make(chan struct{}) c.closeReadMu.Unlock() go func() { + defer close(c.closeReadDone) defer cancel() defer c.close() _, _, err := c.Reader(ctx) From 43abf8ed82390611189393dd2547ed9bd675a956 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 7 Apr 2024 08:18:44 -0700 Subject: [PATCH 151/152] README.md: Revert assembly change for now --- README.md | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index cde3ec6d..d093746d 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ go get nhooyr.io/websocket - [RFC 7692](https://tools.ietf.org/html/rfc7692) permessage-deflate compression - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections - Compile to [Wasm](https://pkg.go.dev/nhooyr.io/websocket#hdr-Wasm) -- WebSocket masking implemented in assembly for amd64 and arm64 [#326](https://github.com/nhooyr/websocket/issues/326) ## Roadmap @@ -37,6 +36,8 @@ See GitHub issues for minor issues but the major future enhancements are: - [ ] Ping pong heartbeat helper [#267](https://github.com/nhooyr/websocket/issues/267) - [ ] Ping pong instrumentation callbacks [#246](https://github.com/nhooyr/websocket/issues/246) - [ ] Graceful shutdown helpers [#209](https://github.com/nhooyr/websocket/issues/209) +- [ ] Assembly for WebSocket masking [#16](https://github.com/nhooyr/websocket/issues/16) + - WIP at [#326](https://github.com/nhooyr/websocket/pull/326), about 3x faster - [ ] HTTP/2 [#4](https://github.com/nhooyr/websocket/issues/4) - [ ] The holy grail [#402](https://github.com/nhooyr/websocket/issues/402) @@ -120,8 +121,9 @@ Advantages of nhooyr.io/websocket: - Gorilla requires registering a pong callback before sending a Ping - Can target Wasm ([gorilla/websocket#432](https://github.com/gorilla/websocket/issues/432)) - Transparent message buffer reuse with [wsjson](https://pkg.go.dev/nhooyr.io/websocket/wsjson) subpackage -- [3.5x](https://github.com/nhooyr/websocket/pull/326#issuecomment-1959470758) faster WebSocket masking implementation in assembly for amd64 and arm64 and [2x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster implementation in pure Go +- [1.75x](https://github.com/nhooyr/websocket/releases/tag/v1.7.4) faster WebSocket masking implementation in pure Go - Gorilla's implementation is slower and uses [unsafe](https://golang.org/pkg/unsafe/). + Soon we'll have assembly and be 3x faster [#326](https://github.com/nhooyr/websocket/pull/326) - Full [permessage-deflate](https://tools.ietf.org/html/rfc7692) compression extension support - Gorilla only supports no context takeover mode - [CloseRead](https://pkg.go.dev/nhooyr.io/websocket#Conn.CloseRead) helper for write only connections ([gorilla/websocket#492](https://github.com/gorilla/websocket/issues/492)) From e87d61ad4a680ccb5d0fabcdf5dceb2e154ece64 Mon Sep 17 00:00:00 2001 From: Anmol Sethi Date: Sun, 7 Apr 2024 09:12:54 -0700 Subject: [PATCH 152/152] Misc fixes for release --- .github/workflows/ci.yml | 8 ++++---- .github/workflows/daily.yml | 8 ++++---- close.go | 2 +- conn_test.go | 2 +- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3c650580..e9b4b5f6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: ./ci/fmt.sh @@ -19,7 +19,7 @@ jobs: steps: - uses: actions/checkout@v4 - run: go version - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: ./ci/lint.sh @@ -28,7 +28,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: ./ci/test.sh @@ -41,7 +41,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: ./ci/bench.sh diff --git a/.github/workflows/daily.yml b/.github/workflows/daily.yml index 340de501..2ba9ce34 100644 --- a/.github/workflows/daily.yml +++ b/.github/workflows/daily.yml @@ -12,7 +12,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/bench.sh @@ -20,7 +20,7 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/test.sh @@ -34,7 +34,7 @@ jobs: - uses: actions/checkout@v4 with: ref: dev - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/bench.sh @@ -44,7 +44,7 @@ jobs: - uses: actions/checkout@v4 with: ref: dev - - uses: actions/setup-go@v4 + - uses: actions/setup-go@v5 with: go-version-file: ./go.mod - run: AUTOBAHN=1 ./ci/test.sh diff --git a/close.go b/close.go index 625ed121..31504b0e 100644 --- a/close.go +++ b/close.go @@ -131,7 +131,7 @@ func (c *Conn) Close(code StatusCode, reason string) (err error) { // CloseNow closes the WebSocket connection without attempting a close handshake. // Use when you do not want the overhead of the close handshake. func (c *Conn) CloseNow() (err error) { - defer errd.Wrap(&err, "failed to close WebSocket") + defer errd.Wrap(&err, "failed to immediately close WebSocket") if !c.casClosing() { err = c.waitGoroutines() diff --git a/conn_test.go b/conn_test.go index 9fbe961d..2b44ad22 100644 --- a/conn_test.go +++ b/conn_test.go @@ -346,7 +346,7 @@ func TestConn(t *testing.T) { func TestWasm(t *testing.T) { t.Parallel() if os.Getenv("CI") == "" { - t.Skip() + t.SkipNow() } s := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {