Skip to content

Commit 555110f

Browse files
committed
Lock manages file locks
1 parent 98a0108 commit 555110f

File tree

2 files changed

+177
-0
lines changed

2 files changed

+177
-0
lines changed

extensions/internal/lock/lock.go

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,75 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
package lock
5+
6+
import (
7+
"context"
8+
"errors"
9+
"fmt"
10+
"os"
11+
"path/filepath"
12+
"time"
13+
14+
"github.com/AzureAD/microsoft-authentication-extensions-for-go/extensions/internal/flock"
15+
)
16+
17+
// timeout lets tests set the default amount of time allowed to acquire the lock
18+
var timeout = 5 * time.Second
19+
20+
// flocker helps tests fake flock
21+
type flocker interface {
22+
Fh() *os.File
23+
Path() string
24+
TryLockContext(context.Context, time.Duration) (bool, error)
25+
Unlock() error
26+
}
27+
28+
// Lock uses a file lock to coordinate access to resources shared with other processes.
29+
// Callers are responsible for preventing races within a process.
30+
type Lock struct {
31+
f flocker
32+
retryDelay time.Duration
33+
}
34+
35+
// New is the constructor for Lock. "p" is the path to the lock file.
36+
func New(p string, retryDelay time.Duration) (*Lock, error) {
37+
// ensure all dirs in the path exist before flock tries to create the file
38+
err := os.MkdirAll(filepath.Dir(p), os.ModePerm)
39+
if err != nil {
40+
return nil, err
41+
}
42+
return &Lock{f: flock.New(p), retryDelay: retryDelay}, nil
43+
}
44+
45+
// Lock acquires the file lock on behalf of the process. The behavior of concurrent
46+
// and repeated calls is undefined. For example, Linux may or may not allow goroutines
47+
// scheduled on different threads to hold the lock simultaneously.
48+
func (l *Lock) Lock(ctx context.Context) error {
49+
if _, hasDeadline := ctx.Deadline(); !hasDeadline {
50+
var cancel context.CancelFunc
51+
ctx, cancel = context.WithTimeout(ctx, timeout)
52+
defer cancel()
53+
}
54+
locked, err := l.f.TryLockContext(ctx, l.retryDelay)
55+
if err != nil {
56+
return err
57+
}
58+
if locked {
59+
_, _ = l.f.Fh().WriteString(fmt.Sprintf("{%d} {%s}", os.Getpid(), os.Args[0]))
60+
return nil
61+
}
62+
return errors.New("couldn't acquire file lock")
63+
}
64+
65+
// Unlock releases the lock and deletes the lock file.
66+
func (l *Lock) Unlock() error {
67+
err := l.f.Unlock()
68+
if err == nil {
69+
err = os.Remove(l.f.Path())
70+
}
71+
if errors.Is(err, os.ErrNotExist) {
72+
return nil
73+
}
74+
return err
75+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
// Copyright (c) Microsoft Corporation. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
package lock
5+
6+
import (
7+
"context"
8+
"errors"
9+
"os"
10+
"path/filepath"
11+
"runtime"
12+
"testing"
13+
"time"
14+
15+
"github.com/stretchr/testify/require"
16+
)
17+
18+
var ctx = context.Background()
19+
20+
type fakeFlock struct {
21+
err error
22+
p string
23+
}
24+
25+
func (f fakeFlock) Fh() *os.File {
26+
fh, _ := os.Open(f.p)
27+
return fh
28+
}
29+
30+
func (f fakeFlock) Path() string {
31+
return f.p
32+
}
33+
34+
func (f fakeFlock) TryLockContext(context.Context, time.Duration) (bool, error) {
35+
return f.err == nil, f.err
36+
}
37+
38+
func (f fakeFlock) Unlock() error {
39+
return f.err
40+
}
41+
42+
func TestCreatesAndRemovesFile(t *testing.T) {
43+
p := filepath.Join(t.TempDir(), "nonexistent", t.Name())
44+
lock, err := New(p, 0)
45+
require.NoError(t, err)
46+
require.NoFileExists(t, p)
47+
48+
err = lock.Lock(ctx)
49+
require.NoError(t, err)
50+
require.FileExists(t, p, "Lock didn't create the file")
51+
52+
err = lock.Unlock()
53+
require.NoError(t, err)
54+
require.NoFileExists(t, p, "Unlock didn't remove the file")
55+
}
56+
57+
func TestLockError(t *testing.T) {
58+
p := filepath.Join(t.TempDir(), t.Name())
59+
lock, err := New(p, 0)
60+
require.NoError(t, err)
61+
expected := errors.New("expected")
62+
lock.f = fakeFlock{err: expected}
63+
require.Equal(t, lock.Lock(ctx), expected)
64+
}
65+
66+
func TestLockTimeout(t *testing.T) {
67+
defer func(d time.Duration) { timeout = d }(timeout)
68+
timeout = 0
69+
70+
p := filepath.Join(t.TempDir(), t.Name())
71+
a, err := New(p, 0)
72+
require.NoError(t, err)
73+
err = a.Lock(ctx)
74+
require.NoError(t, err)
75+
b, err := New(p, 0)
76+
require.NoError(t, err)
77+
78+
err = b.Lock(ctx)
79+
require.ErrorIs(t, err, context.DeadlineExceeded)
80+
81+
require.NoError(t, a.Unlock())
82+
}
83+
84+
func TestUnlockErrors(t *testing.T) {
85+
p := filepath.Join(t.TempDir(), t.Name())
86+
lock, err := New(p, 0)
87+
require.NoError(t, err)
88+
89+
err = lock.Lock(ctx)
90+
require.NoError(t, err)
91+
if runtime.GOOS != "windows" {
92+
// Remove would fail on Windows because the file lock is mandatory there
93+
require.NoError(t, os.Remove(p))
94+
}
95+
// Unlock should return nil even when the lock file has been removed
96+
require.NoError(t, lock.Unlock())
97+
98+
expected := errors.New("it didn't work")
99+
lock.f = fakeFlock{err: expected}
100+
actual := lock.Unlock()
101+
require.Equal(t, expected, actual)
102+
}

0 commit comments

Comments
 (0)