Skip to content

Commit

Permalink
Merge pull request etcd-io#4871 from gyuho/windows_file_lock_20160326
Browse files Browse the repository at this point in the history
pkg/fileutil: lock file on Windows
  • Loading branch information
gyuho committed Mar 27, 2016
2 parents fa98d8d + 3f1a1c3 commit 83ada72
Show file tree
Hide file tree
Showing 3 changed files with 110 additions and 9 deletions.
6 changes: 4 additions & 2 deletions pkg/fileutil/fileutil_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import (
"os/user"
"path/filepath"
"reflect"
"runtime"
"testing"
)

Expand All @@ -41,10 +42,11 @@ func TestIsDirWriteable(t *testing.T) {
// http://stackoverflow.com/questions/20609415/cross-compiling-user-current-not-implemented-on-linux-amd64
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.
// 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 {
t.Fatalf("expected IsDirWriteable to error")
Expand Down
101 changes: 97 additions & 4 deletions pkg/fileutil/lock_windows.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,110 @@

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) {
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) {
// TODO make this actually work
f, err := os.OpenFile(path, flag, perm)
f, err := open(path, flag, perm)
if err != nil {
return nil, err
}
if err := lockFile(syscall.Handle(f.Fd()), 0); err != nil {
f.Close()
return nil, err
}
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
}
12 changes: 9 additions & 3 deletions pkg/fileutil/purge_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,12 @@ func TestPurgeFile(t *testing.T) {
defer os.RemoveAll(dir)

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 {
t.Fatal(err)
}
f.Close()
}

stop := make(chan struct{})
Expand All @@ -45,10 +47,12 @@ func TestPurgeFile(t *testing.T) {

// create 5 more files
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 {
t.Fatal(err)
}
f.Close()
time.Sleep(10 * time.Millisecond)
}

Expand Down Expand Up @@ -88,10 +92,12 @@ func TestPurgeFileHoldingLockFile(t *testing.T) {
defer os.RemoveAll(dir)

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 {
t.Fatal(err)
}
f.Close()
}

// create a purge barrier at 5
Expand Down

0 comments on commit 83ada72

Please sign in to comment.