fix: build plugins/common on windows

On windows, `LockDirectory` now takes a `LockFileEx` lock on a `.dokku.lock` file inside the directory, as `flock` is unavailable there.
This commit is contained in:
Jose Diaz-Gonzalez
2026-11-08 21:13:25 -05:00
parent 12c1bf3493
commit 06a6fffb59
5 changed files with 91 additions and 20 deletions

View File

@@ -13,6 +13,7 @@ require (
github.com/spf13/pflag v1.1.0 github.com/spf13/pflag v1.1.0
golang.org/x/crypto v0.58.0 golang.org/x/crypto v0.58.0
golang.org/x/sync v0.24.0 golang.org/x/sync v0.24.0
golang.org/x/sys v0.49.0
mvdan.cc/sh/v3 v3.14.1 mvdan.cc/sh/v3 v3.14.1
) )
@@ -28,6 +29,5 @@ require (
github.com/stretchr/testify v1.12.1 // indirect github.com/stretchr/testify v1.12.1 // indirect
go.yaml.in/yaml/v3 v3.0.5 // indirect go.yaml.in/yaml/v3 v3.0.5 // indirect
golang.org/x/net v0.60.0 // indirect golang.org/x/net v0.60.0 // indirect
golang.org/x/sys v0.49.0 // indirect
golang.org/x/text v0.43.0 // indirect golang.org/x/text v0.43.0 // indirect
) )

View File

@@ -1,11 +1,9 @@
package common package common
import ( import (
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
"syscall"
) )
// LockedDirectoriesEnvVar names the environment variable a caller holding a // LockedDirectoriesEnvVar names the environment variable a caller holding a
@@ -15,11 +13,13 @@ import (
// does not deadlock waiting on its own parent. // does not deadlock waiting on its own parent.
const LockedDirectoriesEnvVar = "DOKKU_LOCKED_DIRECTORIES" const LockedDirectoriesEnvVar = "DOKKU_LOCKED_DIRECTORIES"
// LockDirectory holds an exclusive flock on dir for the duration of fn. The // LockDirectory holds an exclusive lock on dir for the duration of fn. The
// directory must already exist. The lock is advisory: it serializes callers // directory must already exist. On unix the lock is a flock on the directory
// that go through LockDirectory, and does not block plain reads. Locks are // itself, and on windows it is a LockFileEx lock on a lock file inside the
// taken per open file description, so goroutines in a single process exclude // directory. The lock is advisory: it serializes callers that go through
// each other as well as separate processes do. // LockDirectory, and does not block plain reads. Locks are taken per open
// file, so goroutines in a single process exclude each other as well as
// separate processes do.
// //
// A directory listed in DOKKU_LOCKED_DIRECTORIES is treated as already held by // A directory listed in DOKKU_LOCKED_DIRECTORIES is treated as already held by
// a parent process, and fn runs without taking the lock again. LockDirectory is // a parent process, and fn runs without taking the lock again. LockDirectory is
@@ -31,18 +31,7 @@ func LockDirectory(dir string, fn func() error) error {
return fn() return fn()
} }
file, err := os.Open(dir) return lockDirectory(dir, fn)
if err != nil {
return fmt.Errorf("Unable to open %s for locking: %w", dir, err)
}
defer file.Close()
if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
return fmt.Errorf("Unable to lock %s: %w", dir, err)
}
defer syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
return fn()
} }
// isDirectoryLockHeld reports whether dir is listed in DOKKU_LOCKED_DIRECTORIES // isDirectoryLockHeld reports whether dir is listed in DOKKU_LOCKED_DIRECTORIES

View File

@@ -0,0 +1,25 @@
//go:build !windows
package common
import (
"fmt"
"os"
"syscall"
)
// lockDirectory holds an exclusive flock on dir for the duration of fn
func lockDirectory(dir string, fn func() error) error {
file, err := os.Open(dir)
if err != nil {
return fmt.Errorf("Unable to open %s for locking: %w", dir, err)
}
defer file.Close()
if err := syscall.Flock(int(file.Fd()), syscall.LOCK_EX); err != nil {
return fmt.Errorf("Unable to lock %s: %w", dir, err)
}
defer syscall.Flock(int(file.Fd()), syscall.LOCK_UN)
return fn()
}

View File

@@ -0,0 +1,35 @@
//go:build windows
package common
import (
"fmt"
"os"
"path/filepath"
"golang.org/x/sys/windows"
)
// directoryLockFile names the file inside a locked directory that holds the
// lock on windows, where a directory handle cannot be locked directly. The
// file is left in place after unlocking, as removing it would race other
// callers waiting on the lock.
const directoryLockFile = ".dokku.lock"
// lockDirectory holds an exclusive LockFileEx lock on the lock file inside dir
// for the duration of fn
func lockDirectory(dir string, fn func() error) error {
file, err := os.OpenFile(filepath.Join(dir, directoryLockFile), os.O_RDWR|os.O_CREATE, 0600)
if err != nil {
return fmt.Errorf("Unable to open %s for locking: %w", dir, err)
}
defer file.Close()
handle := windows.Handle(file.Fd())
if err := windows.LockFileEx(handle, windows.LOCKFILE_EXCLUSIVE_LOCK, 0, 1, 0, &windows.Overlapped{}); err != nil {
return fmt.Errorf("Unable to lock %s: %w", dir, err)
}
defer windows.UnlockFileEx(handle, 0, 1, 0, &windows.Overlapped{})
return fn()
}

View File

@@ -7,6 +7,7 @@ import (
"path/filepath" "path/filepath"
"strconv" "strconv"
"sync" "sync"
"sync/atomic"
"testing" "testing"
"time" "time"
@@ -269,6 +270,27 @@ func TestPropertyLockSerializesReadModifyWrite(t *testing.T) {
Expect(PropertyGet(propertyTestPlugin, propertyTestApp, "counter")).To(Equal("50")) Expect(PropertyGet(propertyTestPlugin, propertyTestApp, "counter")).To(Equal("50"))
} }
func TestLockDirectorySerializesConcurrentCallers(t *testing.T) {
RegisterTestingT(t)
t.Setenv(LockedDirectoriesEnvVar, "")
dir := t.TempDir()
var active atomic.Int32
var overlapped atomic.Bool
Expect(runConcurrently(20, func(i int) error {
return LockDirectory(dir, func() error {
if active.Add(1) > 1 {
overlapped.Store(true)
}
defer active.Add(-1)
time.Sleep(5 * time.Millisecond)
return nil
})
})).To(Succeed())
Expect(overlapped.Load()).To(BeFalse())
}
func TestLockDirectorySkipsDirectoriesHeldByParent(t *testing.T) { func TestLockDirectorySkipsDirectoriesHeldByParent(t *testing.T) {
setupPropertyMapTest(t) setupPropertyMapTest(t)