}
func (o *operation) overlapped() *syscall.Overlapped {
- if o.runtimeCtx == 0 {
+ if o.fd.isBlocking {
// Don't return the overlapped object if the file handle
// doesn't use overlapped I/O. It could be used, but
// that would then use the file pointer stored in the
if err != nil {
return 0, err
}
+ getOverlappedResult := func() (int, error) {
+ if fd.isFile {
+ err = windows.GetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false)
+ } else {
+ err = windows.WSAGetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false, &o.flags)
+ }
+ switch err {
+ case nil:
+ return int(o.qty), nil
+ case syscall.ERROR_HANDLE_EOF:
+ // EOF reached.
+ return int(o.qty), io.EOF
+ case syscall.ERROR_MORE_DATA, windows.WSAEMSGSIZE:
+ // More data available. Return back the size of received data.
+ return int(o.qty), err
+ default:
+ return 0, err
+ }
+ }
// Start IO.
err = submit(o)
if !fd.pd.pollable() {
+ if err == syscall.ERROR_IO_PENDING {
+ // The overlapped handle is not added to the runtime poller,
+ // the only way to wait for the IO to complete is block.
+ _, err = syscall.WaitForSingleObject(fd.Sysfd, syscall.INFINITE)
+ if err == nil {
+ return getOverlappedResult()
+ }
+ }
if err != nil {
return 0, err
}
// Wait for our request to complete.
err = fd.pd.wait(int(o.mode), fd.isFile)
if err == nil {
- if fd.isFile {
- err = windows.GetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false)
- } else {
- err = windows.WSAGetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false, &o.flags)
- }
// All is good. Extract our IO results and return.
- if err != nil {
- // More data available. Return back the size of received data.
- if err == syscall.ERROR_MORE_DATA || err == windows.WSAEMSGSIZE {
- return int(o.qty), err
- }
- return 0, err
- }
- return int(o.qty), nil
+ return getOverlappedResult()
}
// IO is interrupted by "close" or "timeout"
netpollErr := err
}
// Wait for cancellation to complete.
fd.pd.waitCanceled(int(o.mode))
- if fd.isFile {
- err = windows.GetOverlappedResult(fd.Sysfd, &o.o, &o.qty, true)
- } else {
- err = windows.WSAGetOverlappedResult(fd.Sysfd, &o.o, &o.qty, false, &o.flags)
- }
+ n, err := getOverlappedResult()
if err != nil {
if err == syscall.ERROR_OPERATION_ABORTED { // IO Canceled
err = netpollErr
}
- return 0, err
+ return n, err
}
// We issued a cancellation request. But, it seems, IO operation succeeded
// before the cancellation request run. We need to treat the IO operation as
// succeeded (the bytes are actually sent/recv from network).
- return int(o.qty), nil
+ return n, nil
}
// FD is a file descriptor. The net and os packages embed this type in
// The kind of this file.
kind fileKind
+
+ // Whether FILE_FLAG_OVERLAPPED was not set when opening the file
+ isBlocking bool
}
// setOffset sets the offset fields of the overlapped object
// If we could not add the handle to the runtime poller,
// assume the handle hasn't been opened for overlapped I/O.
err = fd.pd.init(fd)
+ pollable = err == nil
}
if logInitFD != nil {
logInitFD(net, fd, err)
}
- if !pollable || err != nil {
+ if !pollable {
+ // Handle opened for overlapped I/O (aka non-blocking) that are not added
+ // to the runtime poller need special handling when reading and writing.
+ var info windows.FILE_MODE_INFORMATION
+ if err := windows.NtQueryInformationFile(fd.Sysfd, &windows.IO_STATUS_BLOCK{}, uintptr(unsafe.Pointer(&info)), uint32(unsafe.Sizeof(info)), windows.FileModeInformation); err == nil {
+ fd.isBlocking = info.Mode&(windows.FILE_SYNCHRONOUS_IO_ALERT|windows.FILE_SYNCHRONOUS_IO_NONALERT) != 0
+ } else {
+ // If we fail to get the file mode information, assume the file is blocking.
+ fd.isBlocking = true
+ }
return err
}
if fd.kind != kindNet || socketCanUseSetFileCompletionNotificationModes {
return syscall.ReadFile(o.fd.Sysfd, unsafe.Slice(o.buf.Buf, o.buf.Len), &o.qty, o.overlapped())
})
fd.addOffset(n)
+ if err == syscall.ERROR_HANDLE_EOF {
+ err = io.EOF
+ }
if fd.kind == kindPipe && err != nil {
switch err {
case syscall.ERROR_BROKEN_PIPE:
return syscall.ReadFile(o.fd.Sysfd, unsafe.Slice(o.buf.Buf, o.buf.Len), &o.qty, &o.o)
})
if err != nil {
- n = 0
if err == syscall.ERROR_HANDLE_EOF {
err = io.EOF
}
SynRetrans uint8
}
-func newFD(t testing.TB, h syscall.Handle, kind string, overlapped bool) *poll.FD {
+func newFD(t testing.TB, h syscall.Handle, kind string, overlapped, pollable bool) *poll.FD {
fd := poll.FD{
Sysfd: h,
IsStream: true,
ZeroReadIsEOF: true,
}
- err := fd.Init(kind, true)
+ err := fd.Init(kind, pollable)
if overlapped && err != nil {
// Overlapped file handles should not error.
t.Fatal(err)
- } else if !overlapped && err == nil {
+ } else if !overlapped && pollable && err == nil {
// Non-overlapped file handles should return an error but still
// be usable as sync handles.
t.Fatal("expected error for non-overlapped file handle")
return &fd
}
-func newFile(t testing.TB, name string, overlapped bool) *poll.FD {
+func newFile(t testing.TB, name string, overlapped, pollable bool) *poll.FD {
namep, err := syscall.UTF16PtrFromString(name)
if err != nil {
t.Fatal(err)
t.Fatal(err)
}
})
- return newFD(t, h, "file", overlapped)
+ typ, err := syscall.GetFileType(h)
+ if err != nil {
+ t.Fatal(err)
+ }
+ kind := "file"
+ if typ == syscall.FILE_TYPE_PIPE {
+ kind = "pipe"
+ }
+ return newFD(t, h, kind, overlapped, pollable)
}
var currentProces = sync.OnceValue(func() string {
var pipeCounter atomic.Uint64
-func newPipe(t testing.TB, overlapped, message bool) (string, *poll.FD) {
- name := `\\.\pipe\go-internal-poll-test-` + currentProces() + `-` + strconv.FormatUint(pipeCounter.Add(1), 10)
+func newBytePipe(t testing.TB, name string, overlapped, pollable bool) *poll.FD {
+ return newPipe(t, name, false, overlapped, pollable)
+}
+
+func newMessagePipe(t testing.TB, name string, overlapped, pollable bool) *poll.FD {
+ return newPipe(t, name, true, overlapped, pollable)
+}
+
+func pipeName() string {
+ return `\\.\pipe\go-internal-poll-test-` + currentProces() + `-` + strconv.FormatUint(pipeCounter.Add(1), 10)
+}
+
+func newPipe(t testing.TB, name string, message, overlapped, pollable bool) *poll.FD {
wname, err := syscall.UTF16PtrFromString(name)
if err != nil {
t.Fatal(err)
t.Fatal(err)
}
})
- return name, newFD(t, h, "pipe", overlapped)
+ return newFD(t, h, "pipe", overlapped, pollable)
}
func testReadWrite(t *testing.T, fdr, fdw *poll.FD) {
func TestFile(t *testing.T) {
t.Parallel()
- test := func(t *testing.T, r, w bool) {
- t.Parallel()
- name := filepath.Join(t.TempDir(), "foo")
- rh := newFile(t, name, r)
- wh := newFile(t, name, w)
- testReadWrite(t, rh, wh)
- testPreadPwrite(t, rh, wh)
- }
- t.Run("overlapped", func(t *testing.T) {
- test(t, true, true)
- })
- t.Run("overlapped-read", func(t *testing.T) {
- test(t, true, false)
- })
- t.Run("overlapped-write", func(t *testing.T) {
- test(t, false, true)
- })
- t.Run("sync", func(t *testing.T) {
- test(t, false, false)
- })
+ tests := []struct {
+ name string
+ overlappedRead bool
+ overlappedWrite bool
+ pollable bool
+ }{
+ {"overlapped", true, true, true},
+ {"overlapped-nonpollable", true, true, false},
+ {"overlapped-read", true, false, true},
+ {"overlapped-write", false, true, true},
+ {"sync", false, false, false},
+ {"sync-pollable", false, false, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ name := filepath.Join(t.TempDir(), "foo")
+ rh := newFile(t, name, tt.overlappedRead, tt.pollable)
+ wh := newFile(t, name, tt.overlappedWrite, tt.pollable)
+ testReadWrite(t, rh, wh)
+ testPreadPwrite(t, rh, wh)
+ })
+ }
}
func TestPipe(t *testing.T) {
t.Parallel()
- t.Run("overlapped", func(t *testing.T) {
- t.Parallel()
- name, pipe := newPipe(t, true, false)
- file := newFile(t, name, true)
- testReadWrite(t, pipe, file)
- })
- t.Run("overlapped-write", func(t *testing.T) {
- t.Parallel()
- name, pipe := newPipe(t, true, false)
- file := newFile(t, name, false)
- testReadWrite(t, file, pipe)
- })
- t.Run("overlapped-read", func(t *testing.T) {
- t.Parallel()
- name, pipe := newPipe(t, false, false)
- file := newFile(t, name, true)
- testReadWrite(t, file, pipe)
- })
- t.Run("sync", func(t *testing.T) {
- t.Parallel()
- name, pipe := newPipe(t, false, false)
- file := newFile(t, name, false)
- testReadWrite(t, file, pipe)
- })
+ tests := []struct {
+ name string
+ overlappedRead bool
+ overlappedWrite bool
+ pollable bool
+ }{
+ {"overlapped", true, true, true},
+ {"overlapped-nonpollable", true, true, false},
+ {"overlapped-write", false, true, true},
+ {"overlapped-read", true, false, true},
+ {"sync", false, false, false},
+ {"sync-pollable", false, false, true},
+ }
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ t.Parallel()
+ name := pipeName()
+ pipe := newBytePipe(t, name, tt.overlappedWrite, tt.pollable)
+ file := newFile(t, name, tt.overlappedRead, tt.pollable)
+ testReadWrite(t, pipe, file)
+ })
+ }
t.Run("anonymous", func(t *testing.T) {
t.Parallel()
var r, w syscall.Handle
}
}()
// CreatePipe always returns sync handles.
- fdr := newFD(t, r, "pipe", false)
- fdw := newFD(t, w, "file", false)
+ fdr := newFD(t, r, "pipe", false, false)
+ fdw := newFD(t, w, "file", false, false)
testReadWrite(t, fdr, fdw)
})
}
func TestPipeWriteEOF(t *testing.T) {
t.Parallel()
- name, pipe := newPipe(t, false, true)
- file := newFile(t, name, false)
+ name := pipeName()
+ pipe := newMessagePipe(t, name, false, true)
+ file := newFile(t, name, false, true)
read := make(chan struct{}, 1)
go func() {
_, err := pipe.Write(nil)
func TestPipeCanceled(t *testing.T) {
t.Parallel()
- name, _ := newPipe(t, true, false)
- file := newFile(t, name, true)
+ name := pipeName()
+ _ = newBytePipe(t, name, true, true)
+ file := newFile(t, name, true, true)
ch := make(chan struct{}, 1)
go func() {
for {
if err != nil {
b.Fatal(err)
}
- file := newFile(b, name, overlapped)
+ file := newFile(b, name, overlapped, true)
var buf [len(content)]byte
for b.Loop() {
_, err := io.ReadFull(file, buf[:])
package os_test
import (
+ "bytes"
"errors"
"fmt"
"internal/godebug"
"path/filepath"
"runtime"
"slices"
+ "strconv"
"strings"
+ "sync"
+ "sync/atomic"
"syscall"
"testing"
"unicode/utf16"
t.Errorf("SameFile(%v, %v) = false; want true", f2, f2s)
}
}
+
+func TestReadWriteFileOverlapped(t *testing.T) {
+ // See https://go.dev/issue/15388.
+ t.Parallel()
+
+ name := filepath.Join(t.TempDir(), "test.txt")
+ wname, err := syscall.UTF16PtrFromString(name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ h, err := syscall.CreateFile(wname, syscall.GENERIC_ALL, 0, nil, syscall.CREATE_NEW, syscall.FILE_ATTRIBUTE_NORMAL|syscall.FILE_FLAG_OVERLAPPED, 0)
+ if err != nil {
+ t.Fatal(err)
+ }
+ f := os.NewFile(uintptr(h), name)
+ defer f.Close()
+
+ data := []byte("test")
+ n, err := f.Write(data)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if n != len(data) {
+ t.Fatalf("Write = %d; want %d", n, len(data))
+ }
+
+ if _, err := f.Seek(0, io.SeekStart); err != nil {
+ t.Fatal(err)
+ }
+
+ got, err := io.ReadAll(f)
+ if err != nil {
+ t.Fatal(err)
+ }
+ if !bytes.Equal(got, data) {
+ t.Fatalf("Read = %q; want %q", got, data)
+ }
+}
+
+var currentProces = sync.OnceValue(func() string {
+ // Convert the process ID to a string.
+ return strconv.FormatUint(uint64(os.Getpid()), 10)
+})
+
+var pipeCounter atomic.Uint64
+
+func pipeName() string {
+ return `\\.\pipe\go-os-test-` + currentProces() + `-` + strconv.FormatUint(pipeCounter.Add(1), 10)
+}
+
+func createPipe(t *testing.T, name string, inherit bool) *os.File {
+ t.Helper()
+ wname, err := syscall.UTF16PtrFromString(name)
+ if err != nil {
+ t.Fatal(err)
+ }
+ flags := windows.PIPE_ACCESS_DUPLEX | syscall.FILE_FLAG_OVERLAPPED
+ typ := windows.PIPE_TYPE_BYTE
+ sa := &syscall.SecurityAttributes{
+ Length: uint32(unsafe.Sizeof(syscall.SecurityAttributes{})),
+ }
+ if inherit {
+ sa.InheritHandle = 1
+ }
+ rh, err := windows.CreateNamedPipe(wname, uint32(flags), uint32(typ), 1, 4096, 4096, 0, sa)
+ if err != nil {
+ t.Fatal(err)
+ }
+ return os.NewFile(uintptr(rh), name)
+}
+
+func TestStdinOverlappedPipe(t *testing.T) {
+ // Test that we can read from a named pipe open with FILE_FLAG_OVERLAPPED.
+ // See https://go.dev/issue/15388.
+ if os.Getenv("GO_WANT_HELPER_PROCESS") == "1" {
+ var buf string
+ _, err := fmt.Scanln(&buf)
+ if err != nil {
+ fmt.Print(err)
+ os.Exit(1)
+ }
+ fmt.Println(buf)
+ os.Exit(0)
+ }
+
+ t.Parallel()
+ name := pipeName()
+
+ // Create the read handle inherited by the child process.
+ r := createPipe(t, name, true)
+ defer r.Close()
+
+ // Create a write handle.
+ w, err := os.OpenFile(name, os.O_WRONLY, 0666)
+ if err != nil {
+ t.Fatal(err)
+ }
+ defer w.Close()
+
+ // Write some data to the pipe. The child process will read it.
+ want := []byte("test\n")
+ if _, err := w.Write(want); err != nil {
+ t.Fatal(err)
+ }
+
+ // Create a child process that will read from the pipe
+ // and write the data to stdout.
+ cmd := testenv.Command(t, testenv.Executable(t), fmt.Sprintf("-test.run=^%s$", t.Name()), "-test.v")
+ cmd = testenv.CleanCmdEnv(cmd)
+ cmd.Env = append(cmd.Env, "GO_WANT_HELPER_PROCESS=1")
+ cmd.Stdin = r
+ got, err := cmd.CombinedOutput()
+ if err != nil {
+ t.Fatalf("running %q failed: %v\n%s", cmd, err, got)
+ }
+
+ if !bytes.Contains(got, want) {
+ t.Fatalf("output %q does not contain %q", got, want)
+ }
+}