Browse Source

Merge pull request #4871 from gyuho/windows_file_lock_20160326

pkg/fileutil: lock file on Windows
Gyu-Ho Lee 9 years ago
parent
commit
83ada7232a
3 changed files with 110 additions and 9 deletions
  1. 4 2
      pkg/fileutil/fileutil_test.go
  2. 97 4
      pkg/fileutil/lock_windows.go
  3. 9 3
      pkg/fileutil/purge_test.go

+ 4 - 2
pkg/fileutil/fileutil_test.go

@@ -20,6 +20,7 @@ import (
 	"os/user"
 	"os/user"
 	"path/filepath"
 	"path/filepath"
 	"reflect"
 	"reflect"
+	"runtime"
 	"testing"
 	"testing"
 )
 )
 
 
@@ -41,10 +42,11 @@ func TestIsDirWriteable(t *testing.T) {
 		// http://stackoverflow.com/questions/20609415/cross-compiling-user-current-not-implemented-on-linux-amd64
 		// http://stackoverflow.com/questions/20609415/cross-compiling-user-current-not-implemented-on-linux-amd64
 		t.Skipf("failed to get current user: %v", err)
 		t.Skipf("failed to get current user: %v", err)
 	}
 	}
-	if me.Name == "root" || me.Name == "Administrator" {
+	if me.Name == "root" || runtime.GOOS == "windows" {
 		// ideally we should check CAP_DAC_OVERRIDE.
 		// ideally we should check CAP_DAC_OVERRIDE.
 		// but it does not matter for tests.
 		// but it does not matter for tests.
-		t.Skipf("running as a superuser")
+		// Chmod is not supported under windows.
+		t.Skipf("running as a superuser or in windows")
 	}
 	}
 	if err := IsDirWriteable(tmpdir); err == nil {
 	if err := IsDirWriteable(tmpdir); err == nil {
 		t.Fatalf("expected IsDirWriteable to error")
 		t.Fatalf("expected IsDirWriteable to error")

+ 97 - 4
pkg/fileutil/lock_windows.go

@@ -16,17 +16,110 @@
 
 
 package fileutil
 package fileutil
 
 
-import "os"
+import (
+	"errors"
+	"fmt"
+	"os"
+	"syscall"
+	"unsafe"
+)
+
+var (
+	modkernel32    = syscall.NewLazyDLL("kernel32.dll")
+	procLockFileEx = modkernel32.NewProc("LockFileEx")
+
+	errLocked = errors.New("The process cannot access the file because another process has locked a portion of the file.")
+)
+
+const (
+	// https://msdn.microsoft.com/en-us/library/windows/desktop/aa365203(v=vs.85).aspx
+	LOCKFILE_EXCLUSIVE_LOCK   = 2
+	LOCKFILE_FAIL_IMMEDIATELY = 1
+
+	// see https://msdn.microsoft.com/en-us/library/windows/desktop/ms681382(v=vs.85).aspx
+	errLockViolation syscall.Errno = 0x21
+)
 
 
 func TryLockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
 func TryLockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
-	return LockFile(path, flag, perm)
+	f, err := open(path, flag, perm)
+	if err != nil {
+		return nil, err
+	}
+	if err := lockFile(syscall.Handle(f.Fd()), LOCKFILE_FAIL_IMMEDIATELY); err != nil {
+		f.Close()
+		return nil, err
+	}
+	return &LockedFile{f}, nil
 }
 }
 
 
 func LockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
 func LockFile(path string, flag int, perm os.FileMode) (*LockedFile, error) {
-	// TODO make this actually work
-	f, err := os.OpenFile(path, flag, perm)
+	f, err := open(path, flag, perm)
 	if err != nil {
 	if err != nil {
 		return nil, err
 		return nil, err
 	}
 	}
+	if err := lockFile(syscall.Handle(f.Fd()), 0); err != nil {
+		f.Close()
+		return nil, err
+	}
 	return &LockedFile{f}, nil
 	return &LockedFile{f}, nil
 }
 }
+
+func open(path string, flag int, perm os.FileMode) (*os.File, error) {
+	if path == "" {
+		return nil, fmt.Errorf("cannot open empty filename")
+	}
+	var access uint32
+	switch flag {
+	case syscall.O_RDONLY:
+		access = syscall.GENERIC_READ
+	case syscall.O_WRONLY:
+		access = syscall.GENERIC_WRITE
+	case syscall.O_RDWR:
+		access = syscall.GENERIC_READ | syscall.GENERIC_WRITE
+	case syscall.O_WRONLY | syscall.O_CREAT:
+		access = syscall.GENERIC_ALL
+	default:
+		panic(fmt.Errorf("flag %v is not supported", flag))
+	}
+	fd, err := syscall.CreateFile(&(syscall.StringToUTF16(path)[0]),
+		access,
+		syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE,
+		nil,
+		syscall.OPEN_ALWAYS,
+		syscall.FILE_ATTRIBUTE_NORMAL,
+		0)
+	if err != nil {
+		return nil, err
+	}
+	return os.NewFile(uintptr(fd), path), nil
+}
+
+func lockFile(fd syscall.Handle, flags uint32) error {
+	var flag uint32 = LOCKFILE_EXCLUSIVE_LOCK
+	flag |= flags
+	if fd == syscall.InvalidHandle {
+		return nil
+	}
+	err := lockFileEx(fd, flag, 1, 0, &syscall.Overlapped{})
+	if err == nil {
+		return nil
+	} else if err.Error() == errLocked.Error() {
+		return ErrLocked
+	} else if err != errLockViolation {
+		return err
+	}
+	return nil
+}
+
+func lockFileEx(h syscall.Handle, flags, locklow, lockhigh uint32, ol *syscall.Overlapped) (err error) {
+	var reserved uint32 = 0
+	r1, _, e1 := syscall.Syscall6(procLockFileEx.Addr(), 6, uintptr(h), uintptr(flags), uintptr(reserved), uintptr(locklow), uintptr(lockhigh), uintptr(unsafe.Pointer(ol)))
+	if r1 == 0 {
+		if e1 != 0 {
+			err = error(e1)
+		} else {
+			err = syscall.EINVAL
+		}
+	}
+	return
+}

+ 9 - 3
pkg/fileutil/purge_test.go

@@ -32,10 +32,12 @@ func TestPurgeFile(t *testing.T) {
 	defer os.RemoveAll(dir)
 	defer os.RemoveAll(dir)
 
 
 	for i := 0; i < 5; i++ {
 	for i := 0; i < 5; i++ {
-		_, err = os.Create(path.Join(dir, fmt.Sprintf("%d.test", i)))
+		var f *os.File
+		f, err = os.Create(path.Join(dir, fmt.Sprintf("%d.test", i)))
 		if err != nil {
 		if err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
 		}
 		}
+		f.Close()
 	}
 	}
 
 
 	stop := make(chan struct{})
 	stop := make(chan struct{})
@@ -45,10 +47,12 @@ func TestPurgeFile(t *testing.T) {
 
 
 	// create 5 more files
 	// create 5 more files
 	for i := 5; i < 10; i++ {
 	for i := 5; i < 10; i++ {
-		_, err = os.Create(path.Join(dir, fmt.Sprintf("%d.test", i)))
+		var f *os.File
+		f, err = os.Create(path.Join(dir, fmt.Sprintf("%d.test", i)))
 		if err != nil {
 		if err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
 		}
 		}
+		f.Close()
 		time.Sleep(10 * time.Millisecond)
 		time.Sleep(10 * time.Millisecond)
 	}
 	}
 
 
@@ -88,10 +92,12 @@ func TestPurgeFileHoldingLockFile(t *testing.T) {
 	defer os.RemoveAll(dir)
 	defer os.RemoveAll(dir)
 
 
 	for i := 0; i < 10; i++ {
 	for i := 0; i < 10; i++ {
-		_, err = os.Create(path.Join(dir, fmt.Sprintf("%d.test", i)))
+		var f *os.File
+		f, err = os.Create(path.Join(dir, fmt.Sprintf("%d.test", i)))
 		if err != nil {
 		if err != nil {
 			t.Fatal(err)
 			t.Fatal(err)
 		}
 		}
+		f.Close()
 	}
 	}
 
 
 	// create a purge barrier at 5
 	// create a purge barrier at 5