Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 21 additions & 1 deletion lib/utils/fs.go
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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))
Expand All @@ -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) {
Expand Down
52 changes: 52 additions & 0 deletions lib/utils/fs_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,9 +20,11 @@ package utils

import (
"context"
"fmt"
"os"
"path/filepath"
"runtime"
"sync/atomic"
"testing"
"time"

Expand Down Expand Up @@ -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")
Expand Down