github.com/Serizao/go-winio@v0.0.0-20230906082528-f02f7f4ad6e8/file.go (about)

     1  //go:build windows
     2  // +build windows
     3  
     4  package winio
     5  
     6  import (
     7  	"errors"
     8  	"io"
     9  	"runtime"
    10  	"sync"
    11  	"sync/atomic"
    12  	"syscall"
    13  	"time"
    14  
    15  	"golang.org/x/sys/windows"
    16  )
    17  
    18  //sys cancelIoEx(file windows.Handle, o *windows.Overlapped) (err error) = CancelIoEx
    19  //sys createIoCompletionPort(file windows.Handle, port windows.Handle, key uintptr, threadCount uint32) (newport windows.Handle, err error) = CreateIoCompletionPort
    20  //sys getQueuedCompletionStatus(port windows.Handle, bytes *uint32, key *uintptr, o **ioOperation, timeout uint32) (err error) = GetQueuedCompletionStatus
    21  //sys setFileCompletionNotificationModes(h windows.Handle, flags uint8) (err error) = SetFileCompletionNotificationModes
    22  //sys wsaGetOverlappedResult(h windows.Handle, o *windows.Overlapped, bytes *uint32, wait bool, flags *uint32) (err error) = ws2_32.WSAGetOverlappedResult
    23  
    24  //todo (go1.19): switch to [atomic.Bool]
    25  
    26  type atomicBool int32
    27  
    28  func (b *atomicBool) isSet() bool { return atomic.LoadInt32((*int32)(b)) != 0 }
    29  func (b *atomicBool) setFalse()   { atomic.StoreInt32((*int32)(b), 0) }
    30  func (b *atomicBool) setTrue()    { atomic.StoreInt32((*int32)(b), 1) }
    31  
    32  //revive:disable-next-line:predeclared Keep "new" to maintain consistency with "atomic" pkg
    33  func (b *atomicBool) swap(new bool) bool {
    34  	var newInt int32
    35  	if new {
    36  		newInt = 1
    37  	}
    38  	return atomic.SwapInt32((*int32)(b), newInt) == 1
    39  }
    40  
    41  var (
    42  	ErrFileClosed = errors.New("file has already been closed")
    43  	ErrTimeout    = &timeoutError{}
    44  )
    45  
    46  type timeoutError struct{}
    47  
    48  func (*timeoutError) Error() string   { return "i/o timeout" }
    49  func (*timeoutError) Timeout() bool   { return true }
    50  func (*timeoutError) Temporary() bool { return true }
    51  
    52  type timeoutChan chan struct{}
    53  
    54  var ioInitOnce sync.Once
    55  var ioCompletionPort windows.Handle
    56  
    57  // ioResult contains the result of an asynchronous IO operation.
    58  type ioResult struct {
    59  	bytes uint32
    60  	err   error
    61  }
    62  
    63  // ioOperation represents an outstanding asynchronous Win32 IO.
    64  type ioOperation struct {
    65  	o  windows.Overlapped
    66  	ch chan ioResult
    67  }
    68  
    69  func initIO() {
    70  	h, err := createIoCompletionPort(windows.InvalidHandle, 0, 0, 0xffffffff)
    71  	if err != nil {
    72  		panic(err)
    73  	}
    74  	ioCompletionPort = h
    75  	go ioCompletionProcessor(h)
    76  }
    77  
    78  // win32File implements Reader, Writer, and Closer on a Win32 handle without blocking in a syscall.
    79  // It takes ownership of this handle and will close it if it is garbage collected.
    80  type win32File struct {
    81  	handle        windows.Handle
    82  	wg            sync.WaitGroup
    83  	wgLock        sync.RWMutex
    84  	closing       atomicBool
    85  	socket        bool
    86  	readDeadline  deadlineHandler
    87  	writeDeadline deadlineHandler
    88  }
    89  
    90  type deadlineHandler struct {
    91  	setLock     sync.Mutex
    92  	channel     timeoutChan
    93  	channelLock sync.RWMutex
    94  	timer       *time.Timer
    95  	timedout    atomicBool
    96  }
    97  
    98  // makeWin32File makes a new win32File from an existing file handle.
    99  func makeWin32File(h windows.Handle) (*win32File, error) {
   100  	f := &win32File{handle: h}
   101  	ioInitOnce.Do(initIO)
   102  	_, err := createIoCompletionPort(h, ioCompletionPort, 0, 0xffffffff)
   103  	if err != nil {
   104  		return nil, err
   105  	}
   106  	err = setFileCompletionNotificationModes(h, windows.FILE_SKIP_COMPLETION_PORT_ON_SUCCESS|windows.FILE_SKIP_SET_EVENT_ON_HANDLE)
   107  	if err != nil {
   108  		return nil, err
   109  	}
   110  	f.readDeadline.channel = make(timeoutChan)
   111  	f.writeDeadline.channel = make(timeoutChan)
   112  	return f, nil
   113  }
   114  
   115  // Deprecated: use NewOpenFile instead.
   116  func MakeOpenFile(h syscall.Handle) (io.ReadWriteCloser, error) {
   117  	return NewOpenFile(windows.Handle(h))
   118  }
   119  
   120  func NewOpenFile(h windows.Handle) (io.ReadWriteCloser, error) {
   121  	// If we return the result of makeWin32File directly, it can result in an
   122  	// interface-wrapped nil, rather than a nil interface value.
   123  	f, err := makeWin32File(h)
   124  	if err != nil {
   125  		return nil, err
   126  	}
   127  	return f, nil
   128  }
   129  
   130  // closeHandle closes the resources associated with a Win32 handle.
   131  func (f *win32File) closeHandle() {
   132  	f.wgLock.Lock()
   133  	// Atomically set that we are closing, releasing the resources only once.
   134  	if !f.closing.swap(true) {
   135  		f.wgLock.Unlock()
   136  		// cancel all IO and wait for it to complete
   137  		_ = cancelIoEx(f.handle, nil)
   138  		f.wg.Wait()
   139  		// at this point, no new IO can start
   140  		windows.Close(f.handle)
   141  		f.handle = 0
   142  	} else {
   143  		f.wgLock.Unlock()
   144  	}
   145  }
   146  
   147  // Close closes a win32File.
   148  func (f *win32File) Close() error {
   149  	f.closeHandle()
   150  	return nil
   151  }
   152  
   153  // IsClosed checks if the file has been closed.
   154  func (f *win32File) IsClosed() bool {
   155  	return f.closing.isSet()
   156  }
   157  
   158  // prepareIO prepares for a new IO operation.
   159  // The caller must call f.wg.Done() when the IO is finished, prior to Close() returning.
   160  func (f *win32File) prepareIO() (*ioOperation, error) {
   161  	f.wgLock.RLock()
   162  	if f.closing.isSet() {
   163  		f.wgLock.RUnlock()
   164  		return nil, ErrFileClosed
   165  	}
   166  	f.wg.Add(1)
   167  	f.wgLock.RUnlock()
   168  	c := &ioOperation{}
   169  	c.ch = make(chan ioResult)
   170  	return c, nil
   171  }
   172  
   173  // ioCompletionProcessor processes completed async IOs forever.
   174  func ioCompletionProcessor(h windows.Handle) {
   175  	for {
   176  		var bytes uint32
   177  		var key uintptr
   178  		var op *ioOperation
   179  		err := getQueuedCompletionStatus(h, &bytes, &key, &op, windows.INFINITE)
   180  		if op == nil {
   181  			panic(err)
   182  		}
   183  		op.ch <- ioResult{bytes, err}
   184  	}
   185  }
   186  
   187  // todo: helsaawy - create an asyncIO version that takes a context
   188  
   189  // asyncIO processes the return value from ReadFile or WriteFile, blocking until
   190  // the operation has actually completed.
   191  func (f *win32File) asyncIO(c *ioOperation, d *deadlineHandler, bytes uint32, err error) (int, error) {
   192  	if err != windows.ERROR_IO_PENDING { //nolint:errorlint // err is Errno
   193  		return int(bytes), err
   194  	}
   195  
   196  	if f.closing.isSet() {
   197  		_ = cancelIoEx(f.handle, &c.o)
   198  	}
   199  
   200  	var timeout timeoutChan
   201  	if d != nil {
   202  		d.channelLock.Lock()
   203  		timeout = d.channel
   204  		d.channelLock.Unlock()
   205  	}
   206  
   207  	var r ioResult
   208  	select {
   209  	case r = <-c.ch:
   210  		err = r.err
   211  		if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
   212  			if f.closing.isSet() {
   213  				err = ErrFileClosed
   214  			}
   215  		} else if err != nil && f.socket {
   216  			// err is from Win32. Query the overlapped structure to get the winsock error.
   217  			var bytes, flags uint32
   218  			err = wsaGetOverlappedResult(f.handle, &c.o, &bytes, false, &flags)
   219  		}
   220  	case <-timeout:
   221  		_ = cancelIoEx(f.handle, &c.o)
   222  		r = <-c.ch
   223  		err = r.err
   224  		if err == windows.ERROR_OPERATION_ABORTED { //nolint:errorlint // err is Errno
   225  			err = ErrTimeout
   226  		}
   227  	}
   228  
   229  	// runtime.KeepAlive is needed, as c is passed via native
   230  	// code to ioCompletionProcessor, c must remain alive
   231  	// until the channel read is complete.
   232  	// todo: (de)allocate *ioOperation via win32 heap functions, instead of needing to KeepAlive?
   233  	runtime.KeepAlive(c)
   234  	return int(r.bytes), err
   235  }
   236  
   237  // Read reads from a file handle.
   238  func (f *win32File) Read(b []byte) (int, error) {
   239  	c, err := f.prepareIO()
   240  	if err != nil {
   241  		return 0, err
   242  	}
   243  	defer f.wg.Done()
   244  
   245  	if f.readDeadline.timedout.isSet() {
   246  		return 0, ErrTimeout
   247  	}
   248  
   249  	var bytes uint32
   250  	err = windows.ReadFile(f.handle, b, &bytes, &c.o)
   251  	n, err := f.asyncIO(c, &f.readDeadline, bytes, err)
   252  	runtime.KeepAlive(b)
   253  
   254  	// Handle EOF conditions.
   255  	if err == nil && n == 0 && len(b) != 0 {
   256  		return 0, io.EOF
   257  	} else if err == windows.ERROR_BROKEN_PIPE { //nolint:errorlint // err is Errno
   258  		return 0, io.EOF
   259  	} else {
   260  		return n, err
   261  	}
   262  }
   263  
   264  // Write writes to a file handle.
   265  func (f *win32File) Write(b []byte) (int, error) {
   266  	c, err := f.prepareIO()
   267  	if err != nil {
   268  		return 0, err
   269  	}
   270  	defer f.wg.Done()
   271  
   272  	if f.writeDeadline.timedout.isSet() {
   273  		return 0, ErrTimeout
   274  	}
   275  
   276  	var bytes uint32
   277  	err = windows.WriteFile(f.handle, b, &bytes, &c.o)
   278  	n, err := f.asyncIO(c, &f.writeDeadline, bytes, err)
   279  	runtime.KeepAlive(b)
   280  	return n, err
   281  }
   282  
   283  func (f *win32File) SetReadDeadline(deadline time.Time) error {
   284  	return f.readDeadline.set(deadline)
   285  }
   286  
   287  func (f *win32File) SetWriteDeadline(deadline time.Time) error {
   288  	return f.writeDeadline.set(deadline)
   289  }
   290  
   291  func (f *win32File) Flush() error {
   292  	return windows.FlushFileBuffers(f.handle)
   293  }
   294  
   295  func (f *win32File) Fd() uintptr {
   296  	return uintptr(f.handle)
   297  }
   298  
   299  func (d *deadlineHandler) set(deadline time.Time) error {
   300  	d.setLock.Lock()
   301  	defer d.setLock.Unlock()
   302  
   303  	if d.timer != nil {
   304  		if !d.timer.Stop() {
   305  			<-d.channel
   306  		}
   307  		d.timer = nil
   308  	}
   309  	d.timedout.setFalse()
   310  
   311  	select {
   312  	case <-d.channel:
   313  		d.channelLock.Lock()
   314  		d.channel = make(chan struct{})
   315  		d.channelLock.Unlock()
   316  	default:
   317  	}
   318  
   319  	if deadline.IsZero() {
   320  		return nil
   321  	}
   322  
   323  	timeoutIO := func() {
   324  		d.timedout.setTrue()
   325  		close(d.channel)
   326  	}
   327  
   328  	now := time.Now()
   329  	duration := deadline.Sub(now)
   330  	if deadline.After(now) {
   331  		// Deadline is in the future, set a timer to wait
   332  		d.timer = time.AfterFunc(duration, timeoutIO)
   333  	} else {
   334  		// Deadline is in the past. Cancel all pending IO now.
   335  		timeoutIO()
   336  	}
   337  	return nil
   338  }