Skip to content

Commit

Permalink
vsock: reintroduce Go 1.11 support
Browse files Browse the repository at this point in the history
  • Loading branch information
mdlayher committed Mar 15, 2019
1 parent 4e70ebb commit 8d8cc86
Show file tree
Hide file tree
Showing 8 changed files with 148 additions and 54 deletions.
1 change: 1 addition & 0 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
language: go
go:
- 1.x
- 1.11.x
os:
- linux
sudo: required
Expand Down
26 changes: 22 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,15 +6,33 @@ communication between a hypervisor and its virtual machines. MIT Licensed.
For more information about VM sockets, check out my blog about
[Linux VM sockets in Go](https://medium.com/@mdlayher/linux-vm-sockets-in-go-ea11768e9e67).

## Go version support

This package supports varying levels of functionality depending on the version
of Go used during compilation. The `net.Listener` and `net.Conn` types produced
by this package are backed by non-blocking I/O, in order to integrate with Go's
runtime network poller in newer versions of Go.

- **Go 1.12.x+ (recommended):**
- `net.Listener`:
- `Accept` blocks until a connection is received
- `Close` can interrupt `Accept` and make it return a permanent error
- `net.Conn`: full timeout support via `SetDeadline` family of methods
- Go 1.11.x **(not recommended)**:
- `net.Listener`:
- `Accept` is **non-blocking** and should be called in a loop, checking for
`net.Error.Temporary() == true` and sleeping for a short period to avoid
wasteful, spinning CPU cycles
- `Close` makes `Accept` return a permanent error on the next
loop iteration
- `net.Conn`: full timeout support via `SetDeadline` family of methods
- Go 1.10.x or below: **not supported**

## Stability

At this time, package `vsock` is in a pre-v1.0.0 state. Changes are being made
which may impact the exported API of this package and others in its ecosystem.

The general policy of this package is to only support the latest, stable version
of Go. Compatibility shims may be added for prior versions of Go on an as-needed
basis. If you would like to raise a concern, please [file an issue](https://github.com/mdlayher/vsock/issues/new).

**If you depend on this package in your applications, please vendor it or use Go
modules when building your application.**

Expand Down
14 changes: 2 additions & 12 deletions cmd/vscp/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import (
"time"

"github.com/mdlayher/vsock"
"github.com/mdlayher/vsock/internal/vsutil"
)

var (
Expand Down Expand Up @@ -91,25 +92,14 @@ func receive(target string, port uint32, timeout time.Duration) {
}
defer l.Close()

// If a timeout is set, set up a timer to close the listener and unblock
// the call to Accept once the timeout passes.
timeoutCancel := func() {}
if timeout != 0 {
timer := time.AfterFunc(timeout, func() { _ = l.Close() })
timeoutCancel = func() { timer.Stop() }
}

// Show server's address for setting up client flags.
log.Printf("receive: listening: %s", l.Addr())

// Accept a single connection, and receive stream from that connection.
c, err := l.Accept()
c, err := vsutil.Accept(l, timeout)
if err != nil {
fatalf("failed to accept: %v", err)
}

// Got a connection, no need to cancel the listener.
timeoutCancel()
defer c.Close()

if timeout != 0 {
Expand Down
42 changes: 5 additions & 37 deletions fd_linux.go
Original file line number Diff line number Diff line change
Expand Up @@ -67,52 +67,20 @@ func (lfd *sysListenFD) EarlyClose() error { return unix.Close(lfd.fd) }
// Non-blocking mode methods.

func (lfd *sysListenFD) Accept4(flags int) (connFD, unix.Sockaddr, error) {
rc, err := lfd.f.SyscallConn()
if err != nil {
return nil, nil, err
}

var (
nfd int
sa unix.Sockaddr
)

doErr := rc.Read(func(fd uintptr) bool {
nfd, sa, err = unix.Accept4(int(fd), flags)

switch err {
case unix.EAGAIN, unix.ECONNABORTED:
// Return false to let the poller wait for readiness. See the
// source code for internal/poll.FD.RawRead for more details.
//
// When the socket is in non-blocking mode, we might see EAGAIN if
// the socket is not ready for reading.
//
// In addition, the network poller's accept implementation also
// deals with ECONNABORTED, in case a socket is closed before it is
// pulled from our listen queue.
return false
default:
// No error or some unrecognized error, treat this Read operation
// as completed.
return true
}
})
if doErr != nil {
return nil, nil, doErr
}
// Invoke Go version-specific logic for accept.
newFD, sa, err := lfd.accept4(flags)
if err != nil {
return nil, nil, err
}

// Create a non-blocking connFD which will be used to implement net.Conn.
cfd := &sysConnFD{fd: nfd}
cfd := &sysConnFD{fd: newFD}
return cfd, sa, nil
}

func (lfd *sysListenFD) Close() error {
// *os.File.Close will also close the runtime network poller file descriptor,
// so that net.Listener.Accept can stop blocking.
// In Go 1.12+, *os.File.Close will also close the runtime network poller
// file descriptor, so that net.Listener.Accept can stop blocking.
return lfd.f.Close()
}

Expand Down
46 changes: 46 additions & 0 deletions fd_linux_gteq_1.12.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
//+build go1.12,linux

package vsock

import "golang.org/x/sys/unix"

func (lfd *sysListenFD) accept4(flags int) (int, unix.Sockaddr, error) {
// In Go 1.12+, we make use of runtime network poller integration to allow
// net.Listener.Accept to be unblocked by a call to net.Listener.Close.
rc, err := lfd.f.SyscallConn()
if err != nil {
return 0, nil, err
}

var (
newFD int
sa unix.Sockaddr
)

doErr := rc.Read(func(fd uintptr) bool {
newFD, sa, err = unix.Accept4(int(fd), flags)

switch err {
case unix.EAGAIN, unix.ECONNABORTED:
// Return false to let the poller wait for readiness. See the
// source code for internal/poll.FD.RawRead for more details.
//
// When the socket is in non-blocking mode, we might see EAGAIN if
// the socket is not ready for reading.
//
// In addition, the network poller's accept implementation also
// deals with ECONNABORTED, in case a socket is closed before it is
// pulled from our listen queue.
return false
default:
// No error or some unrecognized error, treat this Read operation
// as completed.
return true
}
})
if doErr != nil {
return 0, nil, doErr
}

return newFD, sa, err
}
14 changes: 14 additions & 0 deletions fd_linux_lt_1.12.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
//+build !go1.12,linux

package vsock

import (
"golang.org/x/sys/unix"
)

func (lfd *sysListenFD) accept4(flags int) (int, unix.Sockaddr, error) {
// In Go 1.11, accept on the raw file descriptor directly, because lfd.f
// may be attached to the runtime network poller, forcing this call to block
// even if Close is called.
return unix.Accept4(lfd.fd, flags)
}
42 changes: 42 additions & 0 deletions internal/vsutil/vsutil.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
// Package vsutil provides added functionality for package vsock-internal use.
package vsutil

import (
"net"
"time"
)

// Accept blocks until a single connection is accepted by the net.Listener, and
// then closes the net.Listener. If timeout is non-zero, the listener will be
// closed after the timeout expires, even if no connection was accepted.
func Accept(l net.Listener, timeout time.Duration) (net.Conn, error) {
defer l.Close()

// This function accomodates both Go1.12+ and Go1.11- functionality to allow
// net.Listener.Accept to be canceled by net.Listener.Close.
//
// If a timeout is set, set up a timer to close the listener and either:
// - Go1.12+: unblock the call to Accept
// - Go1.11 : eventually halt the loop due to closed file descriptor
cancel := func() {}
if timeout != 0 {
timer := time.AfterFunc(timeout, func() { _ = l.Close() })
cancel = func() { timer.Stop() }
}

for {
c, err := l.Accept()
if err != nil {
if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
time.Sleep(250 * time.Millisecond)
continue
}

return nil, err
}

// Got a connection, stop the timer.
cancel()
return c, nil
}
}
17 changes: 16 additions & 1 deletion vsock_test.go
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package vsock

import (
"net"
"os"
"strings"
"sync"
"testing"
"time"

"github.com/mdlayher/vsock/internal/vsutil"
)

func TestAddr_fileName(t *testing.T) {
Expand Down Expand Up @@ -71,9 +74,21 @@ func TestUnblockAcceptAfterClose(t *testing.T) {
defer wg.Done()

t.Log("start accept")
_, err := listener.Accept()
_, err := vsutil.Accept(listener, 10*time.Second)
t.Log("after accept")

if nerr, ok := err.(net.Error); ok && nerr.Temporary() {
t.Errorf("expected permanent error, but got temporary one: %v", err)
}

// Go1.11:
if strings.Contains(err.Error(), "bad file descriptor") {
// All is well, the file descriptor was closed.
return
}

// Go 1.12+:
// TODO(mdlayher): wrap string error in net.OpError or similar.
if !strings.Contains(err.Error(), "use of closed file") {
t.Errorf("unexpected accept error: %v", err)
}
Expand Down

0 comments on commit 8d8cc86

Please sign in to comment.