storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/pkg/lock/lock_windows.go (about)

     1  //go:build windows
     2  // +build windows
     3  
     4  /*
     5   * MinIO Cloud Storage, (C) 2016, 2017 MinIO, Inc.
     6   *
     7   * Licensed under the Apache License, Version 2.0 (the "License");
     8   * you may not use this file except in compliance with the License.
     9   * You may obtain a copy of the License at
    10   *
    11   *     http://www.apache.org/licenses/LICENSE-2.0
    12   *
    13   * Unless required by applicable law or agreed to in writing, software
    14   * distributed under the License is distributed on an "AS IS" BASIS,
    15   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    16   * See the License for the specific language governing permissions and
    17   * limitations under the License.
    18   */
    19  
    20  package lock
    21  
    22  import (
    23  	"fmt"
    24  	"os"
    25  	"path/filepath"
    26  	"syscall"
    27  	"unsafe"
    28  
    29  	"golang.org/x/sys/windows"
    30  )
    31  
    32  var (
    33  	modkernel32    = windows.NewLazySystemDLL("kernel32.dll")
    34  	procLockFileEx = modkernel32.NewProc("LockFileEx")
    35  )
    36  
    37  const (
    38  	// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
    39  	lockFileExclusiveLock   = 2
    40  	lockFileFailImmediately = 1
    41  
    42  	// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
    43  	errLockViolation syscall.Errno = 0x21
    44  )
    45  
    46  // lockedOpenFile is an internal function.
    47  func lockedOpenFile(path string, flag int, perm os.FileMode, lockType uint32) (*LockedFile, error) {
    48  	f, err := Open(path, flag, perm)
    49  	if err != nil {
    50  		return nil, err
    51  	}
    52  
    53  	if err = lockFile(syscall.Handle(f.Fd()), lockType); err != nil {
    54  		f.Close()
    55  		return nil, err
    56  	}
    57  
    58  	st, err := os.Stat(path)
    59  	if err != nil {
    60  		f.Close()
    61  		return nil, err
    62  	}
    63  
    64  	if st.IsDir() {
    65  		f.Close()
    66  		return nil, &os.PathError{
    67  			Op:   "open",
    68  			Path: path,
    69  			Err:  syscall.EISDIR,
    70  		}
    71  	}
    72  
    73  	return &LockedFile{File: f}, nil
    74  }
    75  
    76  // TryLockedOpenFile - tries a new write lock, functionality
    77  // it is similar to LockedOpenFile with with syscall.LOCK_EX
    78  // mode but along with syscall.LOCK_NB such that the function
    79  // doesn't wait forever but instead returns if it cannot
    80  // acquire a write lock.
    81  func TryLockedOpenFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
    82  	var lockType uint32 = lockFileFailImmediately | lockFileExclusiveLock
    83  	switch flag {
    84  	case syscall.O_RDONLY:
    85  		// https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-lockfileex
    86  		//lint:ignore SA4016 Reasons
    87  		lockType = lockFileFailImmediately | 0 // Set this to enable shared lock and fail immediately.
    88  	}
    89  	return lockedOpenFile(path, flag, perm, lockType)
    90  }
    91  
    92  // LockedOpenFile - initializes a new lock and protects
    93  // the file from concurrent access.
    94  func LockedOpenFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
    95  	var lockType uint32 = lockFileExclusiveLock
    96  	switch flag {
    97  	case syscall.O_RDONLY:
    98  		// https://docs.microsoft.com/en-us/windows/desktop/api/fileapi/nf-fileapi-lockfileex
    99  		lockType = 0 // Set this to enable shared lock.
   100  	}
   101  	return lockedOpenFile(path, flag, perm, lockType)
   102  }
   103  
   104  // fixLongPath returns the extended-length (\\?\-prefixed) form of
   105  // path when needed, in order to avoid the default 260 character file
   106  // path limit imposed by Windows. If path is not easily converted to
   107  // the extended-length form (for example, if path is a relative path
   108  // or contains .. elements), or is short enough, fixLongPath returns
   109  // path unmodified.
   110  //
   111  // See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
   112  func fixLongPath(path string) string {
   113  	// Do nothing (and don't allocate) if the path is "short".
   114  	// Empirically (at least on the Windows Server 2013 builder),
   115  	// the kernel is arbitrarily okay with < 248 bytes. That
   116  	// matches what the docs above say:
   117  	// "When using an API to create a directory, the specified
   118  	// path cannot be so long that you cannot append an 8.3 file
   119  	// name (that is, the directory name cannot exceed MAX_PATH
   120  	// minus 12)." Since MAX_PATH is 260, 260 - 12 = 248.
   121  	//
   122  	// The MSDN docs appear to say that a normal path that is 248 bytes long
   123  	// will work; empirically the path must be less then 248 bytes long.
   124  	if len(path) < 248 {
   125  		// Don't fix. (This is how Go 1.7 and earlier worked,
   126  		// not automatically generating the \\?\ form)
   127  		return path
   128  	}
   129  
   130  	// The extended form begins with \\?\, as in
   131  	// \\?\c:\windows\foo.txt or \\?\UNC\server\share\foo.txt.
   132  	// The extended form disables evaluation of . and .. path
   133  	// elements and disables the interpretation of / as equivalent
   134  	// to \. The conversion here rewrites / to \ and elides
   135  	// . elements as well as trailing or duplicate separators. For
   136  	// simplicity it avoids the conversion entirely for relative
   137  	// paths or paths containing .. elements. For now,
   138  	// \\server\share paths are not converted to
   139  	// \\?\UNC\server\share paths because the rules for doing so
   140  	// are less well-specified.
   141  	if len(path) >= 2 && path[:2] == `\\` {
   142  		// Don't canonicalize UNC paths.
   143  		return path
   144  	}
   145  	if !filepath.IsAbs(path) {
   146  		// Relative path
   147  		return path
   148  	}
   149  
   150  	const prefix = `\\?`
   151  
   152  	pathbuf := make([]byte, len(prefix)+len(path)+len(`\`))
   153  	copy(pathbuf, prefix)
   154  	n := len(path)
   155  	r, w := 0, len(prefix)
   156  	for r < n {
   157  		switch {
   158  		case os.IsPathSeparator(path[r]):
   159  			// empty block
   160  			r++
   161  		case path[r] == '.' && (r+1 == n || os.IsPathSeparator(path[r+1])):
   162  			// /./
   163  			r++
   164  		case r+1 < n && path[r] == '.' && path[r+1] == '.' && (r+2 == n || os.IsPathSeparator(path[r+2])):
   165  			// /../ is currently unhandled
   166  			return path
   167  		default:
   168  			pathbuf[w] = '\\'
   169  			w++
   170  			for ; r < n && !os.IsPathSeparator(path[r]); r++ {
   171  				pathbuf[w] = path[r]
   172  				w++
   173  			}
   174  		}
   175  	}
   176  	// A drive's root directory needs a trailing \
   177  	if w == len(`\\?\c:`) {
   178  		pathbuf[w] = '\\'
   179  		w++
   180  	}
   181  	return string(pathbuf[:w])
   182  }
   183  
   184  // Open - perm param is ignored, on windows file perms/NT acls
   185  // are not octet combinations. Providing access to NT
   186  // acls is out of scope here.
   187  func Open(path string, flag int, perm os.FileMode) (*os.File, error) {
   188  	if path == "" {
   189  		return nil, syscall.ERROR_FILE_NOT_FOUND
   190  	}
   191  
   192  	pathp, err := syscall.UTF16PtrFromString(fixLongPath(path))
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	var access uint32
   198  	switch flag {
   199  	case syscall.O_RDONLY:
   200  		access = syscall.GENERIC_READ
   201  	case syscall.O_WRONLY:
   202  		access = syscall.GENERIC_WRITE
   203  	case syscall.O_RDWR:
   204  		fallthrough
   205  	case syscall.O_RDWR | syscall.O_CREAT:
   206  		fallthrough
   207  	case syscall.O_WRONLY | syscall.O_CREAT:
   208  		access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
   209  	case syscall.O_WRONLY | syscall.O_CREAT | syscall.O_APPEND:
   210  		access = syscall.FILE_APPEND_DATA
   211  	default:
   212  		return nil, fmt.Errorf("Unsupported flag (%d)", flag)
   213  	}
   214  
   215  	var createflag uint32
   216  	switch {
   217  	case flag&syscall.O_CREAT == syscall.O_CREAT:
   218  		createflag = syscall.OPEN_ALWAYS
   219  	default:
   220  		createflag = syscall.OPEN_EXISTING
   221  	}
   222  
   223  	shareflag := uint32(syscall.FILE_SHARE_READ | syscall.FILE_SHARE_WRITE | syscall.FILE_SHARE_DELETE)
   224  	accessAttr := uint32(syscall.FILE_ATTRIBUTE_NORMAL | 0x80000000)
   225  
   226  	fd, err := syscall.CreateFile(pathp, access, shareflag, nil, createflag, accessAttr, 0)
   227  	if err != nil {
   228  		return nil, err
   229  	}
   230  
   231  	return os.NewFile(uintptr(fd), path), nil
   232  }
   233  
   234  func lockFile(fd syscall.Handle, flags uint32) error {
   235  	// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
   236  	if fd == syscall.InvalidHandle {
   237  		return nil
   238  	}
   239  
   240  	err := lockFileEx(fd, flags, 1, 0, &syscall.Overlapped{})
   241  	if err == nil {
   242  		return nil
   243  	} else if err.Error() == "The process cannot access the file because another process has locked a portion of the file." {
   244  		return ErrAlreadyLocked
   245  	} else if err != errLockViolation {
   246  		return err
   247  	}
   248  
   249  	return nil
   250  }
   251  
   252  func lockFileEx(h syscall.Handle, flags, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
   253  	var reserved = uint32(0)
   254  	r1, _, e1 := syscall.Syscall6(procLockFileEx.Addr(), 6, uintptr(h), uintptr(flags),
   255  		uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)))
   256  	if r1 == 0 {
   257  		if e1 != 0 {
   258  			err = error(e1)
   259  		} else {
   260  			err = syscall.EINVAL
   261  		}
   262  	}
   263  	return
   264  }