Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Expose underlying file's
SyscallConn
method.
This allows raw access to the underlying FD, enabling things like in-kernel copy methods such as `splice` and `tee`. This interface was added to `*os.File` as part of go1.12beta2. Signed-off-by: Brian Goff <cpuguy83@gmail.com>
- Loading branch information
Showing
3 changed files
with
299 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,113 @@ | ||
// +build go1.12 | ||
|
||
package fifo | ||
|
||
import ( | ||
"syscall" | ||
|
||
"github.com/pkg/errors" | ||
) | ||
|
||
// SyscallConn provides raw access to the fifo's underlying filedescrptor. | ||
// See syscall.Conn for guarentees provided by this interface. | ||
func (f *fifo) SyscallConn() (syscall.RawConn, error) { | ||
select { | ||
case <-f.opened: | ||
return newRawConn(f) | ||
default: | ||
} | ||
|
||
if !f.block { | ||
rc := &rawConn{f: f, ready: make(chan struct{})} | ||
go func() { | ||
select { | ||
case <-f.closed: | ||
return | ||
case <-f.opened: | ||
rc.raw, rc.err = f.file.SyscallConn() | ||
close(rc.ready) | ||
} | ||
}() | ||
|
||
return rc, nil | ||
} | ||
|
||
select { | ||
case <-f.opened: | ||
return newRawConn(f) | ||
case <-f.closed: | ||
return nil, errors.New("fifo closed") | ||
} | ||
} | ||
|
||
// newRawConn creates a new syscall.RawConn from a fifo | ||
// | ||
// Note that this assumes the fifo is open | ||
// It is recommended to only call this through `fifo.SyscallConn` | ||
func newRawConn(f *fifo) (syscall.RawConn, error) { | ||
raw, err := f.file.SyscallConn() | ||
if err != nil { | ||
return nil, err | ||
} | ||
|
||
ready := make(chan struct{}) | ||
close(ready) | ||
return &rawConn{f: f, raw: raw, ready: ready}, nil | ||
} | ||
|
||
type rawConn struct { | ||
f *fifo | ||
ready chan struct{} | ||
raw syscall.RawConn | ||
err error | ||
} | ||
|
||
func (r *rawConn) Control(f func(fd uintptr)) error { | ||
select { | ||
case <-r.f.closed: | ||
return errors.New("control of closed fifo") | ||
case <-r.ready: | ||
} | ||
|
||
if r.err != nil { | ||
return r.err | ||
} | ||
|
||
return r.raw.Control(f) | ||
} | ||
|
||
func (r *rawConn) Read(f func(fd uintptr) (done bool)) error { | ||
if r.f.flag&syscall.O_WRONLY > 0 { | ||
return errors.New("reading from write-only fifo") | ||
} | ||
|
||
select { | ||
case <-r.f.closed: | ||
return errors.New("reading of a closed fifo") | ||
case <-r.ready: | ||
} | ||
|
||
if r.err != nil { | ||
return r.err | ||
} | ||
|
||
return r.raw.Read(f) | ||
} | ||
|
||
func (r *rawConn) Write(f func(fd uintptr) (done bool)) error { | ||
if r.f.flag&(syscall.O_WRONLY|syscall.O_RDWR) == 0 { | ||
return errors.New("writing to read-only fifo") | ||
} | ||
|
||
select { | ||
case <-r.f.closed: | ||
return errors.New("writing to a closed fifo") | ||
case <-r.ready: | ||
} | ||
|
||
if r.err != nil { | ||
return r.err | ||
} | ||
|
||
return r.raw.Write(f) | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,184 @@ | ||
// +build go1.12 | ||
|
||
package fifo | ||
|
||
import ( | ||
"bytes" | ||
"io" | ||
"io/ioutil" | ||
"os" | ||
"path" | ||
"path/filepath" | ||
"syscall" | ||
"testing" | ||
"time" | ||
|
||
"github.com/stretchr/testify/assert" | ||
"golang.org/x/net/context" | ||
) | ||
|
||
func TestRawReadWrite(t *testing.T) { | ||
tmpdir, err := ioutil.TempDir("", "fifos") | ||
assert.NoError(t, err) | ||
defer os.RemoveAll(tmpdir) | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | ||
defer cancel() | ||
|
||
r, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600) | ||
assert.NoError(t, err) | ||
defer r.Close() | ||
rawR := makeRawConn(t, r, false) | ||
assert.Error(t, rawR.Write(func(uintptr) bool { return true })) | ||
|
||
w, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_WRONLY|syscall.O_NONBLOCK, 0) | ||
assert.NoError(t, err) | ||
defer w.Close() | ||
rawW := makeRawConn(t, w, false) | ||
assert.Error(t, rawW.Read(func(uintptr) bool { return true })) | ||
|
||
data := []byte("hello world") | ||
rawWrite(t, rawW, data) | ||
|
||
dataR := make([]byte, len(data)) | ||
rawRead(t, rawR, dataR) | ||
assert.True(t, bytes.Equal(data, dataR)) | ||
} | ||
|
||
func TestRawWriteUserRead(t *testing.T) { | ||
tmpdir, err := ioutil.TempDir("", "fifos") | ||
assert.NoError(t, err) | ||
defer os.RemoveAll(tmpdir) | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | ||
defer cancel() | ||
|
||
w, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600) | ||
assert.NoError(t, err) | ||
defer w.Close() | ||
rawW := makeRawConn(t, w, false) | ||
|
||
r, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600) | ||
assert.NoError(t, err) | ||
defer r.Close() | ||
|
||
data := []byte("hello world!") | ||
rawWrite(t, rawW, data) | ||
w.Close() | ||
|
||
buf := make([]byte, len(data)) | ||
n, err := io.ReadFull(r, buf) | ||
assert.NoError(t, err) | ||
assert.True(t, bytes.Equal(data, buf[:n])) | ||
} | ||
|
||
func TestUserWriteRawRead(t *testing.T) { | ||
tmpdir, err := ioutil.TempDir("", "fifos") | ||
assert.NoError(t, err) | ||
defer os.RemoveAll(tmpdir) | ||
|
||
ctx, cancel := context.WithTimeout(context.Background(), 2*time.Second) | ||
defer cancel() | ||
|
||
w, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_WRONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600) | ||
assert.NoError(t, err) | ||
defer w.Close() | ||
|
||
r, err := OpenFifo(ctx, filepath.Join(tmpdir, t.Name()), syscall.O_RDONLY|syscall.O_CREAT|syscall.O_NONBLOCK, 0600) | ||
assert.NoError(t, err) | ||
defer r.Close() | ||
rawR := makeRawConn(t, r, false) | ||
|
||
data := []byte("hello world!") | ||
n, err := w.Write(data) | ||
assert.NoError(t, err) | ||
assert.Equal(t, n, len(data)) | ||
w.Close() | ||
|
||
buf := make([]byte, len(data)) | ||
rawRead(t, rawR, buf) | ||
assert.True(t, bytes.Equal(data, buf[:n])) | ||
} | ||
|
||
func TestRawCloseError(t *testing.T) { | ||
tmpdir, err := ioutil.TempDir("", "fifos") | ||
assert.NoError(t, err) | ||
defer os.RemoveAll(tmpdir) | ||
|
||
t.Run("SyscallConnAfterClose", func(t *testing.T) { | ||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) | ||
defer cancel() | ||
|
||
f, err := OpenFifo(ctx, filepath.Join(tmpdir, path.Base(t.Name())), syscall.O_RDWR|syscall.O_CREAT, 0600) | ||
assert.NoError(t, err) | ||
f.Close() | ||
makeRawConn(t, f, true) | ||
}) | ||
|
||
t.Run("RawOpsAfterClose", func(t *testing.T) { | ||
ctx, cancel := context.WithTimeout(context.Background(), 1*time.Second) | ||
defer cancel() | ||
f, err := OpenFifo(ctx, filepath.Join(tmpdir, path.Base(t.Name())), syscall.O_RDWR|syscall.O_CREAT, 0600) | ||
assert.NoError(t, err) | ||
defer f.Close() | ||
|
||
raw := makeRawConn(t, f, false) | ||
|
||
f.Close() | ||
|
||
assert.Error(t, raw.Control(func(uintptr) {})) | ||
dummy := func(uintptr) bool { return true } | ||
assert.Error(t, raw.Write(dummy)) | ||
assert.Error(t, raw.Read(dummy)) | ||
}) | ||
} | ||
|
||
func makeRawConn(t *testing.T, fifo io.ReadWriteCloser, expectError bool) syscall.RawConn { | ||
sc, ok := fifo.(syscall.Conn) | ||
assert.True(t, ok, "not a syscall.Conn") | ||
|
||
raw, err := sc.SyscallConn() | ||
if !expectError { | ||
assert.NoError(t, err) | ||
} | ||
|
||
return raw | ||
} | ||
|
||
func rawWrite(t *testing.T, rc syscall.RawConn, data []byte) { | ||
var written int | ||
var wErr error | ||
|
||
err := rc.Write(func(fd uintptr) bool { | ||
var n int | ||
n, wErr = syscall.Write(int(fd), data[written:]) | ||
written += n | ||
if wErr != nil || n == 0 || written == len(data) { | ||
return true | ||
} | ||
return false | ||
}) | ||
assert.NoError(t, err) | ||
assert.NoError(t, wErr) | ||
assert.Equal(t, written, len(data)) | ||
} | ||
|
||
func rawRead(t *testing.T, rc syscall.RawConn, data []byte) { | ||
var ( | ||
rErr error | ||
read int | ||
) | ||
|
||
err := rc.Read(func(fd uintptr) bool { | ||
var n int | ||
n, rErr = syscall.Read(int(fd), data[read:]) | ||
read += n | ||
if rErr != nil || n == 0 || read == len(data) { | ||
return true | ||
} | ||
return false | ||
}) | ||
assert.NoError(t, err) | ||
assert.NoError(t, rErr) | ||
assert.Equal(t, read, len(data)) | ||
} |