From d5db7a871314eef86fcc3926662042ccf8054be3 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Mon, 30 Sep 2024 13:17:45 -0400 Subject: [PATCH 1/3] Add filesystem lock for unix and windows platforms --- lib/utils/flock/flock.go | 75 ++++++++++++++++++++++++++ lib/utils/flock/flock_test.go | 77 ++++++++++++++++++++++++++ lib/utils/flock/flock_windows.go | 92 ++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+) create mode 100644 lib/utils/flock/flock.go create mode 100644 lib/utils/flock/flock_test.go create mode 100644 lib/utils/flock/flock_windows.go diff --git a/lib/utils/flock/flock.go b/lib/utils/flock/flock.go new file mode 100644 index 0000000000000..e9ec8b2d1a47f --- /dev/null +++ b/lib/utils/flock/flock.go @@ -0,0 +1,75 @@ +//go:build !windows + +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package flock + +import ( + "context" + "log/slog" + "os" + "syscall" + + "github.com/gravitational/trace" +) + +// Lock implements filesystem lock for blocking another process execution until this lock is released. +func Lock(ctx context.Context, lockFile string) (func(), error) { + // Create the advisory lock using flock. + lf, err := os.OpenFile(lockFile, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return nil, trace.Wrap(err) + } + + rc, err := lf.SyscallConn() + if err != nil { + _ = lf.Close() + return nil, trace.Wrap(err) + } + if err := rc.Control(func(fd uintptr) { + err = syscall.Flock(int(fd), syscall.LOCK_EX) + }); err != nil { + _ = lf.Close() + return nil, trace.Wrap(err) + } + if err != nil { + _ = lf.Close() + return nil, trace.Wrap(err) + } + + return func() { + rc, err := lf.SyscallConn() + if err != nil { + _ = lf.Close() + slog.DebugContext(ctx, "failed to acquire syscall connection", "error", err) + return + } + if err := rc.Control(func(fd uintptr) { + err = syscall.Flock(int(fd), syscall.LOCK_UN) + }); err != nil { + slog.DebugContext(ctx, "failed invokes the control", "file", lockFile, "error", err) + } + if err != nil { + slog.DebugContext(ctx, "failed to unlock file", "file", lockFile, "error", err) + } + if err := lf.Close(); err != nil { + slog.DebugContext(ctx, "failed to close lock file", "file", lockFile, "error", err) + } + }, nil +} diff --git a/lib/utils/flock/flock_test.go b/lib/utils/flock/flock_test.go new file mode 100644 index 0000000000000..0e7b5d51e0522 --- /dev/null +++ b/lib/utils/flock/flock_test.go @@ -0,0 +1,77 @@ +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package flock + +import ( + "context" + "fmt" + "os" + "path/filepath" + "sync/atomic" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +// TestLock verifies that second lock call is blocked until first is released. +func TestLock(t *testing.T) { + var locked atomic.Bool + + ctx := context.Background() + lockFile := filepath.Join(os.TempDir(), ".lock") + t.Cleanup(func() { + require.NoError(t, os.Remove(lockFile)) + }) + + // Acquire first lock should not return any error. + unlock, err := Lock(ctx, lockFile) + require.NoError(t, err) + locked.Store(true) + + signal := make(chan struct{}) + errChan := make(chan error) + go func() { + signal <- struct{}{} + unlock, err := Lock(ctx, lockFile) + if err != nil { + errChan <- err + } + if locked.Load() { + errChan <- fmt.Errorf("first lock is still acquired, second lock must be blocking") + } + unlock() + signal <- struct{}{} + }() + + <-signal + // We have to wait till next lock is reached to ensure we block execution of goroutine. + // Since this is system call we can't track if the function reach blocking state already. + time.Sleep(100 * time.Millisecond) + locked.Store(false) + unlock() + + select { + case <-signal: + case err := <-errChan: + require.NoError(t, err) + case <-time.After(5 * time.Second): + require.Fail(t, "second lock is not released") + } +} diff --git a/lib/utils/flock/flock_windows.go b/lib/utils/flock/flock_windows.go new file mode 100644 index 0000000000000..470f6e5dbb6cb --- /dev/null +++ b/lib/utils/flock/flock_windows.go @@ -0,0 +1,92 @@ +//go:build windows + +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package flock + +import ( + "context" + "log/slog" + "os" + "time" + "unsafe" + + "golang.org/x/sys/windows" + + "github.com/gravitational/trace" +) + +var ( + kernel = windows.NewLazyDLL("kernel32.dll") + proc = kernel.NewProc("CreateFileW") +) + +// Lock implements filesystem lock for blocking another process execution until this lock is released. +func Lock(ctx context.Context, lockFile string) (func(), error) { + lockPath, err := windows.UTF16PtrFromString(lockFile) + if err != nil { + return nil, trace.Wrap(err) + } + + var lf *os.File + for lf == nil { + fd, _, err := proc.Call( + uintptr(unsafe.Pointer(lockPath)), + uintptr(windows.GENERIC_READ|windows.GENERIC_WRITE), + // Exclusive lock, for shared must be used: uintptr(windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE). + uintptr(0), + uintptr(0), + uintptr(windows.OPEN_ALWAYS), + uintptr(windows.FILE_ATTRIBUTE_NORMAL), + 0, + ) + switch err.(windows.Errno) { + case windows.NO_ERROR, windows.ERROR_ALREADY_EXISTS: + lf = os.NewFile(fd, lockFile) + case windows.ERROR_SHARING_VIOLATION: + // if the file is locked by another process we have to wait until the next check. + time.Sleep(time.Second) + default: + windows.CloseHandle(windows.Handle(fd)) + return nil, trace.Wrap(err) + } + } + + rc, err := lf.SyscallConn() + if err != nil { + _ = lf.Close() + return nil, trace.Wrap(err) + } + if err := rc.Control(func(fd uintptr) { + err = windows.SetHandleInformation(windows.Handle(fd), windows.HANDLE_FLAG_INHERIT, 1) + }); err != nil { + _ = lf.Close() + return nil, trace.Wrap(err) + } + if err != nil { + _ = lf.Close() + return nil, trace.Wrap(err) + } + + return func() { + if err := lf.Close(); err != nil { + slog.DebugContext(ctx, "failed to close lock file", "file", lf, "error", err) + } + }, nil +} From 6de91b4a6632d39ca2478e0da07ab09f9e380b34 Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Mon, 30 Sep 2024 14:31:23 -0400 Subject: [PATCH 2/3] Add non-blocking flag --- lib/utils/flock/flock.go | 59 ++++++++----------------------- lib/utils/flock/flock_test.go | 37 ++++++++++++++++---- lib/utils/flock/flock_unix.go | 60 ++++++++++++++++++++++++++++++++ lib/utils/flock/flock_windows.go | 31 +++++++---------- 4 files changed, 117 insertions(+), 70 deletions(-) create mode 100644 lib/utils/flock/flock_unix.go diff --git a/lib/utils/flock/flock.go b/lib/utils/flock/flock.go index e9ec8b2d1a47f..8f52531fc4457 100644 --- a/lib/utils/flock/flock.go +++ b/lib/utils/flock/flock.go @@ -1,5 +1,3 @@ -//go:build !windows - /* * Teleport * Copyright (C) 2024 Gravitational, Inc. @@ -21,55 +19,28 @@ package flock import ( - "context" - "log/slog" + "errors" "os" - "syscall" "github.com/gravitational/trace" ) -// Lock implements filesystem lock for blocking another process execution until this lock is released. -func Lock(ctx context.Context, lockFile string) (func(), error) { - // Create the advisory lock using flock. - lf, err := os.OpenFile(lockFile, os.O_CREATE|os.O_WRONLY, 0666) - if err != nil { - return nil, trace.Wrap(err) - } +var ( + // ErrLocked is returned when file is already locked for non-blocking lock. + ErrLocked = errors.New("file already locked") +) - rc, err := lf.SyscallConn() +// fdSyscall should be used instead of f.Fd() when performing syscalls on fds. +// Context: https://github.com/golang/go/issues/24331 +func fdSyscall(f *os.File, fn func(uintptr) error) error { + rc, err := f.SyscallConn() if err != nil { - _ = lf.Close() - return nil, trace.Wrap(err) + return trace.Wrap(err) } - if err := rc.Control(func(fd uintptr) { - err = syscall.Flock(int(fd), syscall.LOCK_EX) - }); err != nil { - _ = lf.Close() - return nil, trace.Wrap(err) + if cErr := rc.Control(func(fd uintptr) { + err = fn(fd) + }); cErr != nil { + return trace.Wrap(cErr) } - if err != nil { - _ = lf.Close() - return nil, trace.Wrap(err) - } - - return func() { - rc, err := lf.SyscallConn() - if err != nil { - _ = lf.Close() - slog.DebugContext(ctx, "failed to acquire syscall connection", "error", err) - return - } - if err := rc.Control(func(fd uintptr) { - err = syscall.Flock(int(fd), syscall.LOCK_UN) - }); err != nil { - slog.DebugContext(ctx, "failed invokes the control", "file", lockFile, "error", err) - } - if err != nil { - slog.DebugContext(ctx, "failed to unlock file", "file", lockFile, "error", err) - } - if err := lf.Close(); err != nil { - slog.DebugContext(ctx, "failed to close lock file", "file", lockFile, "error", err) - } - }, nil + return trace.Wrap(err) } diff --git a/lib/utils/flock/flock_test.go b/lib/utils/flock/flock_test.go index 0e7b5d51e0522..510bf8eb9d7af 100644 --- a/lib/utils/flock/flock_test.go +++ b/lib/utils/flock/flock_test.go @@ -19,7 +19,6 @@ package flock import ( - "context" "fmt" "os" "path/filepath" @@ -27,6 +26,7 @@ import ( "testing" "time" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" ) @@ -34,14 +34,13 @@ import ( func TestLock(t *testing.T) { var locked atomic.Bool - ctx := context.Background() lockFile := filepath.Join(os.TempDir(), ".lock") t.Cleanup(func() { require.NoError(t, os.Remove(lockFile)) }) // Acquire first lock should not return any error. - unlock, err := Lock(ctx, lockFile) + unlock, err := Lock(lockFile, false) require.NoError(t, err) locked.Store(true) @@ -49,14 +48,19 @@ func TestLock(t *testing.T) { errChan := make(chan error) go func() { signal <- struct{}{} - unlock, err := Lock(ctx, lockFile) + unlock, err := Lock(lockFile, false) if err != nil { errChan <- err + return } if locked.Load() { errChan <- fmt.Errorf("first lock is still acquired, second lock must be blocking") + return + } + if err := unlock(); err != nil { + errChan <- err + return } - unlock() signal <- struct{}{} }() @@ -65,13 +69,32 @@ func TestLock(t *testing.T) { // Since this is system call we can't track if the function reach blocking state already. time.Sleep(100 * time.Millisecond) locked.Store(false) - unlock() + require.NoError(t, unlock()) select { - case <-signal: case err := <-errChan: require.NoError(t, err) + case <-signal: case <-time.After(5 * time.Second): require.Fail(t, "second lock is not released") } } + +// TestLockNonBlock verifies that second lock call returns error until first lock is released. +func TestLockNonBlock(t *testing.T) { + lockfile := filepath.Join(t.TempDir(), ".lock") + unlock, err := Lock(lockfile, true) + require.NoError(t, err) + + _, err = Lock(lockfile, true) + require.Error(t, err) + assert.ErrorIs(t, err, ErrLocked) + + err = unlock() + require.NoError(t, err) + + unlock2, err := Lock(lockfile, true) + require.NoError(t, err) + err = unlock2() + require.NoError(t, err) +} diff --git a/lib/utils/flock/flock_unix.go b/lib/utils/flock/flock_unix.go new file mode 100644 index 0000000000000..2eb5f753b1d8b --- /dev/null +++ b/lib/utils/flock/flock_unix.go @@ -0,0 +1,60 @@ +//go:build !windows + +/* + * Teleport + * Copyright (C) 2024 Gravitational, Inc. + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package flock + +import ( + "errors" + "os" + "path/filepath" + "syscall" + + "github.com/gravitational/trace" +) + +// Lock implements filesystem lock for blocking another process execution until this lock is released. +func Lock(lockFile string, nonblock bool) (func() error, error) { + if err := os.MkdirAll(filepath.Dir(lockFile), 0755); err != nil { + return nil, trace.Wrap(err) + } + + // Create the advisory lock using flock. + lf, err := os.OpenFile(lockFile, os.O_CREATE|os.O_WRONLY, 0666) + if err != nil { + return nil, trace.Wrap(err) + } + how := syscall.LOCK_EX + if nonblock { + how |= syscall.LOCK_NB + } + + err = fdSyscall(lf, func(fd uintptr) error { + return syscall.Flock(int(fd), how) + }) + if errors.Is(err, syscall.EAGAIN) { + return nil, ErrLocked + } + if err != nil { + _ = lf.Close() + return nil, trace.Wrap(err) + } + + return lf.Close, nil +} diff --git a/lib/utils/flock/flock_windows.go b/lib/utils/flock/flock_windows.go index 470f6e5dbb6cb..50cbcace8a3ae 100644 --- a/lib/utils/flock/flock_windows.go +++ b/lib/utils/flock/flock_windows.go @@ -21,9 +21,8 @@ package flock import ( - "context" - "log/slog" "os" + "path/filepath" "time" "unsafe" @@ -38,7 +37,11 @@ var ( ) // Lock implements filesystem lock for blocking another process execution until this lock is released. -func Lock(ctx context.Context, lockFile string) (func(), error) { +func Lock(lockFile string, nonblock bool) (func() error, error) { + if err := os.MkdirAll(filepath.Dir(lockFile), 0755); err != nil { + return nil, trace.Wrap(err) + } + lockPath, err := windows.UTF16PtrFromString(lockFile) if err != nil { return nil, trace.Wrap(err) @@ -60,6 +63,9 @@ func Lock(ctx context.Context, lockFile string) (func(), error) { case windows.NO_ERROR, windows.ERROR_ALREADY_EXISTS: lf = os.NewFile(fd, lockFile) case windows.ERROR_SHARING_VIOLATION: + if nonblock { + return nil, ErrLocked + } // if the file is locked by another process we have to wait until the next check. time.Sleep(time.Second) default: @@ -68,25 +74,12 @@ func Lock(ctx context.Context, lockFile string) (func(), error) { } } - rc, err := lf.SyscallConn() - if err != nil { - _ = lf.Close() - return nil, trace.Wrap(err) - } - if err := rc.Control(func(fd uintptr) { - err = windows.SetHandleInformation(windows.Handle(fd), windows.HANDLE_FLAG_INHERIT, 1) + if err := fdSyscall(lf, func(fd uintptr) error { + return windows.SetHandleInformation(windows.Handle(fd), windows.HANDLE_FLAG_INHERIT, 1) }); err != nil { _ = lf.Close() return nil, trace.Wrap(err) } - if err != nil { - _ = lf.Close() - return nil, trace.Wrap(err) - } - return func() { - if err := lf.Close(); err != nil { - slog.DebugContext(ctx, "failed to close lock file", "file", lf, "error", err) - } - }, nil + return lf.Close, nil } From 524bbf428ce5d1769cacd7ff329b90f7d6f5cf7e Mon Sep 17 00:00:00 2001 From: Vadym Popov Date: Mon, 30 Sep 2024 17:28:03 -0400 Subject: [PATCH 3/3] Replace lock with gofrs/flock --- lib/utils/flock/flock.go | 46 -------------- lib/utils/flock/flock_test.go | 100 ------------------------------- lib/utils/flock/flock_unix.go | 60 ------------------- lib/utils/flock/flock_windows.go | 85 -------------------------- lib/utils/fs.go | 22 ++++++- lib/utils/fs_test.go | 52 ++++++++++++++++ 6 files changed, 73 insertions(+), 292 deletions(-) delete mode 100644 lib/utils/flock/flock.go delete mode 100644 lib/utils/flock/flock_test.go delete mode 100644 lib/utils/flock/flock_unix.go delete mode 100644 lib/utils/flock/flock_windows.go diff --git a/lib/utils/flock/flock.go b/lib/utils/flock/flock.go deleted file mode 100644 index 8f52531fc4457..0000000000000 --- a/lib/utils/flock/flock.go +++ /dev/null @@ -1,46 +0,0 @@ -/* - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package flock - -import ( - "errors" - "os" - - "github.com/gravitational/trace" -) - -var ( - // ErrLocked is returned when file is already locked for non-blocking lock. - ErrLocked = errors.New("file already locked") -) - -// fdSyscall should be used instead of f.Fd() when performing syscalls on fds. -// Context: https://github.com/golang/go/issues/24331 -func fdSyscall(f *os.File, fn func(uintptr) error) error { - rc, err := f.SyscallConn() - if err != nil { - return trace.Wrap(err) - } - if cErr := rc.Control(func(fd uintptr) { - err = fn(fd) - }); cErr != nil { - return trace.Wrap(cErr) - } - return trace.Wrap(err) -} diff --git a/lib/utils/flock/flock_test.go b/lib/utils/flock/flock_test.go deleted file mode 100644 index 510bf8eb9d7af..0000000000000 --- a/lib/utils/flock/flock_test.go +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package flock - -import ( - "fmt" - "os" - "path/filepath" - "sync/atomic" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -// TestLock verifies that second lock call is blocked until first is released. -func TestLock(t *testing.T) { - var locked atomic.Bool - - lockFile := filepath.Join(os.TempDir(), ".lock") - t.Cleanup(func() { - require.NoError(t, os.Remove(lockFile)) - }) - - // Acquire first lock should not return any error. - unlock, err := Lock(lockFile, false) - require.NoError(t, err) - locked.Store(true) - - signal := make(chan struct{}) - errChan := make(chan error) - go func() { - signal <- struct{}{} - unlock, err := Lock(lockFile, false) - if err != nil { - errChan <- err - return - } - if locked.Load() { - errChan <- fmt.Errorf("first lock is still acquired, second lock must be blocking") - return - } - if err := unlock(); err != nil { - errChan <- err - return - } - signal <- struct{}{} - }() - - <-signal - // We have to wait till next lock is reached to ensure we block execution of goroutine. - // Since this is system call we can't track if the function reach blocking state already. - time.Sleep(100 * time.Millisecond) - locked.Store(false) - require.NoError(t, unlock()) - - select { - case err := <-errChan: - require.NoError(t, err) - case <-signal: - case <-time.After(5 * time.Second): - require.Fail(t, "second lock is not released") - } -} - -// TestLockNonBlock verifies that second lock call returns error until first lock is released. -func TestLockNonBlock(t *testing.T) { - lockfile := filepath.Join(t.TempDir(), ".lock") - unlock, err := Lock(lockfile, true) - require.NoError(t, err) - - _, err = Lock(lockfile, true) - require.Error(t, err) - assert.ErrorIs(t, err, ErrLocked) - - err = unlock() - require.NoError(t, err) - - unlock2, err := Lock(lockfile, true) - require.NoError(t, err) - err = unlock2() - require.NoError(t, err) -} diff --git a/lib/utils/flock/flock_unix.go b/lib/utils/flock/flock_unix.go deleted file mode 100644 index 2eb5f753b1d8b..0000000000000 --- a/lib/utils/flock/flock_unix.go +++ /dev/null @@ -1,60 +0,0 @@ -//go:build !windows - -/* - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package flock - -import ( - "errors" - "os" - "path/filepath" - "syscall" - - "github.com/gravitational/trace" -) - -// Lock implements filesystem lock for blocking another process execution until this lock is released. -func Lock(lockFile string, nonblock bool) (func() error, error) { - if err := os.MkdirAll(filepath.Dir(lockFile), 0755); err != nil { - return nil, trace.Wrap(err) - } - - // Create the advisory lock using flock. - lf, err := os.OpenFile(lockFile, os.O_CREATE|os.O_WRONLY, 0666) - if err != nil { - return nil, trace.Wrap(err) - } - how := syscall.LOCK_EX - if nonblock { - how |= syscall.LOCK_NB - } - - err = fdSyscall(lf, func(fd uintptr) error { - return syscall.Flock(int(fd), how) - }) - if errors.Is(err, syscall.EAGAIN) { - return nil, ErrLocked - } - if err != nil { - _ = lf.Close() - return nil, trace.Wrap(err) - } - - return lf.Close, nil -} diff --git a/lib/utils/flock/flock_windows.go b/lib/utils/flock/flock_windows.go deleted file mode 100644 index 50cbcace8a3ae..0000000000000 --- a/lib/utils/flock/flock_windows.go +++ /dev/null @@ -1,85 +0,0 @@ -//go:build windows - -/* - * Teleport - * Copyright (C) 2024 Gravitational, Inc. - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as published by - * the Free Software Foundation, either version 3 of the License, or - * (at your option) any later version. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License - * along with this program. If not, see . - */ - -package flock - -import ( - "os" - "path/filepath" - "time" - "unsafe" - - "golang.org/x/sys/windows" - - "github.com/gravitational/trace" -) - -var ( - kernel = windows.NewLazyDLL("kernel32.dll") - proc = kernel.NewProc("CreateFileW") -) - -// Lock implements filesystem lock for blocking another process execution until this lock is released. -func Lock(lockFile string, nonblock bool) (func() error, error) { - if err := os.MkdirAll(filepath.Dir(lockFile), 0755); err != nil { - return nil, trace.Wrap(err) - } - - lockPath, err := windows.UTF16PtrFromString(lockFile) - if err != nil { - return nil, trace.Wrap(err) - } - - var lf *os.File - for lf == nil { - fd, _, err := proc.Call( - uintptr(unsafe.Pointer(lockPath)), - uintptr(windows.GENERIC_READ|windows.GENERIC_WRITE), - // Exclusive lock, for shared must be used: uintptr(windows.FILE_SHARE_READ|windows.FILE_SHARE_WRITE). - uintptr(0), - uintptr(0), - uintptr(windows.OPEN_ALWAYS), - uintptr(windows.FILE_ATTRIBUTE_NORMAL), - 0, - ) - switch err.(windows.Errno) { - case windows.NO_ERROR, windows.ERROR_ALREADY_EXISTS: - lf = os.NewFile(fd, lockFile) - case windows.ERROR_SHARING_VIOLATION: - if nonblock { - return nil, ErrLocked - } - // if the file is locked by another process we have to wait until the next check. - time.Sleep(time.Second) - default: - windows.CloseHandle(windows.Handle(fd)) - return nil, trace.Wrap(err) - } - } - - if err := fdSyscall(lf, func(fd uintptr) error { - return windows.SetHandleInformation(windows.Handle(fd), windows.HANDLE_FLAG_INHERIT, 1) - }); err != nil { - _ = lf.Close() - return nil, trace.Wrap(err) - } - - return lf.Close, nil -} diff --git a/lib/utils/fs.go b/lib/utils/fs.go index ed4f8e2bbaba4..dbfd1b390d7b7 100644 --- a/lib/utils/fs.go +++ b/lib/utils/fs.go @@ -203,6 +203,16 @@ func FSTryWriteLock(filePath string) (unlock func() error, err error) { return fileLock.Unlock, nil } +// FSWriteLock tries to grab write lock and block if lock is already acquired by someone else. +func FSWriteLock(filePath string) (unlock func() error, err error) { + fileLock := flock.New(getPlatformLockFilePath(filePath)) + if err := fileLock.Lock(); err != nil { + return nil, trace.ConvertSystemError(err) + } + + return fileLock.Unlock, nil +} + // FSTryWriteLockTimeout tries to grab write lock, it's doing it until locks is acquired, or timeout is expired, // or context is expired. func FSTryWriteLockTimeout(ctx context.Context, filePath string, timeout time.Duration) (unlock func() error, err error) { @@ -216,7 +226,7 @@ func FSTryWriteLockTimeout(ctx context.Context, filePath string, timeout time.Du return fileLock.Unlock, nil } -// FSTryReadLock tries to grab write lock, returns ErrUnsuccessfulLockTry +// FSTryReadLock tries to grab shared lock, returns ErrUnsuccessfulLockTry // if lock is already acquired by someone else func FSTryReadLock(filePath string) (unlock func() error, err error) { fileLock := flock.New(getPlatformLockFilePath(filePath)) @@ -231,6 +241,16 @@ func FSTryReadLock(filePath string) (unlock func() error, err error) { return fileLock.Unlock, nil } +// FSReadLock tries to grab shared lock and block if lock is already acquired by someone else. +func FSReadLock(filePath string) (unlock func() error, err error) { + fileLock := flock.New(getPlatformLockFilePath(filePath)) + if err := fileLock.RLock(); err != nil { + return nil, trace.ConvertSystemError(err) + } + + return fileLock.Unlock, nil +} + // FSTryReadLockTimeout tries to grab read lock, it's doing it until locks is acquired, or timeout is expired, // or context is expired. func FSTryReadLockTimeout(ctx context.Context, filePath string, timeout time.Duration) (unlock func() error, err error) { diff --git a/lib/utils/fs_test.go b/lib/utils/fs_test.go index f6ed135ca4069..c2dde216e2f6d 100644 --- a/lib/utils/fs_test.go +++ b/lib/utils/fs_test.go @@ -20,9 +20,11 @@ package utils import ( "context" + "fmt" "os" "path/filepath" "runtime" + "sync/atomic" "testing" "time" @@ -328,6 +330,56 @@ func TestLocks(t *testing.T) { require.NoError(t, unlock()) } +// TestLockWithBlocking verifies that second lock call is blocked until first is released. +func TestLockWithBlocking(t *testing.T) { + var locked atomic.Bool + + lockFile := filepath.Join(os.TempDir(), ".lock") + t.Cleanup(func() { + require.NoError(t, os.Remove(lockFile)) + }) + + // Acquire first lock should not return any error. + unlock, err := FSWriteLock(lockFile) + require.NoError(t, err) + locked.Store(true) + + signal := make(chan struct{}) + errChan := make(chan error) + go func() { + signal <- struct{}{} + unlock, err := FSWriteLock(lockFile) + if err != nil { + errChan <- err + return + } + if locked.Load() { + errChan <- fmt.Errorf("first lock is still acquired, second lock must be blocking") + return + } + if err := unlock(); err != nil { + errChan <- err + return + } + signal <- struct{}{} + }() + + <-signal + // We have to wait till next lock is reached to ensure we block execution of goroutine. + // Since this is system call we can't track if the function reach blocking state already. + time.Sleep(100 * time.Millisecond) + locked.Store(false) + require.NoError(t, unlock()) + + select { + case err := <-errChan: + require.NoError(t, err) + case <-signal: + case <-time.After(5 * time.Second): + require.Fail(t, "second lock is not released") + } +} + func TestOverwriteFile(t *testing.T) { have := []byte("Sensitive Information") fName := filepath.Join(t.TempDir(), "teleport-overwrite-file-test")