Skip to content

Commit

Permalink
Allow non-blocking file locks
Browse files Browse the repository at this point in the history
  • Loading branch information
neonichu committed Jan 19, 2024
1 parent 35afcbf commit 820a789
Show file tree
Hide file tree
Showing 2 changed files with 58 additions and 34 deletions.
35 changes: 26 additions & 9 deletions Sources/TSCBasic/FileSystem.swift
Original file line number Diff line number Diff line change
Expand Up @@ -290,10 +290,10 @@ public protocol FileSystem: Sendable {
func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws

/// Execute the given block while holding the lock.
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () throws -> T) throws -> T
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T

/// Execute the given block while holding the lock.
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () async throws -> T) async throws -> T
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () async throws -> T) async throws -> T
}

/// Convenience implementations (default arguments aren't permitted in protocol
Expand Down Expand Up @@ -338,11 +338,23 @@ public extension FileSystem {
throw FileSystemError(.unsupported, path)
}

func withLock<T>(on path: AbsolutePath, _ body: () throws -> T) throws -> T {
return try withLock(on: path, type: .exclusive, body)
}

func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () throws -> T) throws -> T {
return try withLock(on: path, type: type, blocking: true, body)
}

func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
throw FileSystemError(.unsupported, path)
}

func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () async throws -> T) async throws -> T {
return try await withLock(on: path, type: type, blocking: true, body)
}

func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () async throws -> T) async throws -> T {
throw FileSystemError(.unsupported, path)
}

Expand Down Expand Up @@ -612,16 +624,17 @@ private struct LocalFileSystem: FileSystem {
try FileManager.default.moveItem(at: sourcePath.asURL, to: destinationPath.asURL)
}

func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
try FileLock.withLock(fileToLock: path, type: type, body: body)
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
try FileLock.withLock(fileToLock: path, type: type, blocking: blocking, body: body)
}

func withLock<T>(
on path: AbsolutePath,
type: FileLock.LockType = .exclusive,
type: FileLock.LockType,
blocking: Bool,
_ body: () async throws -> T
) async throws -> T {
try await FileLock.withLock(fileToLock: path, type: type, body: body)
try await FileLock.withLock(fileToLock: path, type: type, blocking: blocking, body: body)
}

func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] {
Expand Down Expand Up @@ -1066,7 +1079,11 @@ public final class InMemoryFileSystem: FileSystem {
}
}

public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
if !blocking {
throw FileSystemError(.unsupported, path)
}

let resolvedPath: AbsolutePath = try lock.withLock {
if case let .symlink(destination) = try getNode(path)?.contents {
return try AbsolutePath(validating: destination, relativeTo: path.parentDirectory)
Expand Down Expand Up @@ -1242,8 +1259,8 @@ public final class RerootedFileSystemView: FileSystem {
try underlyingFileSystem.move(from: formUnderlyingPath(sourcePath), to: formUnderlyingPath(sourcePath))
}

public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
return try underlyingFileSystem.withLock(on: formUnderlyingPath(path), type: type, body)
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
return try underlyingFileSystem.withLock(on: formUnderlyingPath(path), type: type, blocking: blocking, body)
}
}

Expand Down
57 changes: 32 additions & 25 deletions Sources/TSCBasic/Lock.swift
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ extension NSLock {
}
}

enum ProcessLockError: Error {
public enum ProcessLockError: Error {
case unableToAquireLock(errno: Int32)
}

Expand Down Expand Up @@ -89,7 +89,7 @@ public final class FileLock {
/// Try to acquire a lock. This method will block until lock the already aquired by other process.
///
/// Note: This method can throw if underlying POSIX methods fail.
public func lock(type: LockType = .exclusive) throws {
public func lock(type: LockType = .exclusive, blocking: Bool = true) throws {
#if os(Windows)
if handle == nil {
let h: HANDLE = lockFile.pathString.withCString(encodedAs: UTF16.self, {
Expand All @@ -112,17 +112,17 @@ public final class FileLock {
overlapped.Offset = 0
overlapped.OffsetHigh = 0
overlapped.hEvent = nil
var dwFlags = Int32(0)
switch type {
case .exclusive:
if !LockFileEx(handle, DWORD(LOCKFILE_EXCLUSIVE_LOCK), 0,
UInt32.max, UInt32.max, &overlapped) {
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
}
case .shared:
if !LockFileEx(handle, 0, 0,
UInt32.max, UInt32.max, &overlapped) {
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
}
case .exclusive: dwFlags |= LOCKFILE_EXCLUSIVE_LOCK
case .shared: break
}
if !blocking {
dwFlags |= LOCKFILE_FAIL_IMMEDIATELY
}
if !LockFileEx(handle, DWORD(dwFlags), 0,
UInt32.max, UInt32.max, &overlapped) {
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
}
#else
// Open the lock file.
Expand All @@ -133,11 +133,17 @@ public final class FileLock {
}
self.fileDescriptor = fd
}
var flags = Int32(0)
switch type {
case .exclusive: flags = LOCK_EX
case .shared: flags = LOCK_SH
}
if !blocking {
flags |= LOCK_NB
}
// Aquire lock on the file.
while true {
if type == .exclusive && flock(fileDescriptor!, LOCK_EX) == 0 {
break
} else if type == .shared && flock(fileDescriptor!, LOCK_SH) == 0 {
if flock(fileDescriptor!, flags) == 0 {
break
}
// Retry if interrupted.
Expand Down Expand Up @@ -172,23 +178,22 @@ public final class FileLock {
}

/// Execute the given block while holding the lock.
public func withLock<T>(type: LockType = .exclusive, _ body: () throws -> T) throws -> T {
try lock(type: type)
public func withLock<T>(type: LockType = .exclusive, blocking: Bool = true, _ body: () throws -> T) throws -> T {
try lock(type: type, blocking: blocking)
defer { unlock() }
return try body()
}

/// Execute the given block while holding the lock.
public func withLock<T>(type: LockType = .exclusive, _ body: () async throws -> T) async throws -> T {
try lock(type: type)
public func withLock<T>(type: LockType = .exclusive, blocking: Bool = true, _ body: () async throws -> T) async throws -> T {
try lock(type: type, blocking: blocking)
defer { unlock() }
return try await body()
}

private static func prepareLock(
fileToLock: AbsolutePath,
at lockFilesDirectory: AbsolutePath? = nil,
_ type: LockType = .exclusive
at lockFilesDirectory: AbsolutePath? = nil
) throws -> FileLock {
// unless specified, we use the tempDirectory to store lock files
let lockFilesDirectory = try lockFilesDirectory ?? localFileSystem.tempDirectory
Expand Down Expand Up @@ -233,19 +238,21 @@ public final class FileLock {
fileToLock: AbsolutePath,
lockFilesDirectory: AbsolutePath? = nil,
type: LockType = .exclusive,
blocking: Bool = true,
body: () throws -> T
) throws -> T {
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory, type)
return try lock.withLock(type: type, body)
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory)
return try lock.withLock(type: type, blocking: blocking, body)
}

public static func withLock<T>(
fileToLock: AbsolutePath,
lockFilesDirectory: AbsolutePath? = nil,
type: LockType = .exclusive,
blocking: Bool = true,
body: () async throws -> T
) async throws -> T {
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory, type)
return try await lock.withLock(type: type, body)
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory)
return try await lock.withLock(type: type, blocking: blocking, body)
}
}

0 comments on commit 820a789

Please sign in to comment.