Skip to content

Commit 6ba70be

Browse files
authored
Allow non-blocking file locks (#458)
1 parent d9dad59 commit 6ba70be

File tree

2 files changed

+58
-34
lines changed

2 files changed

+58
-34
lines changed

Sources/TSCBasic/FileSystem.swift

+26-9
Original file line numberDiff line numberDiff line change
@@ -292,10 +292,10 @@ public protocol FileSystem: Sendable {
292292
func move(from sourcePath: AbsolutePath, to destinationPath: AbsolutePath) throws
293293

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

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

301301
/// Convenience implementations (default arguments aren't permitted in protocol
@@ -340,11 +340,23 @@ public extension FileSystem {
340340
throw FileSystemError(.unsupported, path)
341341
}
342342

343+
func withLock<T>(on path: AbsolutePath, _ body: () throws -> T) throws -> T {
344+
return try withLock(on: path, type: .exclusive, body)
345+
}
346+
343347
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () throws -> T) throws -> T {
348+
return try withLock(on: path, type: type, blocking: true, body)
349+
}
350+
351+
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
344352
throw FileSystemError(.unsupported, path)
345353
}
346354

347355
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, _ body: () async throws -> T) async throws -> T {
356+
return try await withLock(on: path, type: type, blocking: true, body)
357+
}
358+
359+
func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () async throws -> T) async throws -> T {
348360
throw FileSystemError(.unsupported, path)
349361
}
350362

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

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

619631
func withLock<T>(
620632
on path: AbsolutePath,
621-
type: FileLock.LockType = .exclusive,
633+
type: FileLock.LockType,
634+
blocking: Bool,
622635
_ body: () async throws -> T
623636
) async throws -> T {
624-
try await FileLock.withLock(fileToLock: path, type: type, body: body)
637+
try await FileLock.withLock(fileToLock: path, type: type, blocking: blocking, body: body)
625638
}
626639

627640
func itemReplacementDirectories(for path: AbsolutePath) throws -> [AbsolutePath] {
@@ -1066,7 +1079,11 @@ public final class InMemoryFileSystem: FileSystem {
10661079
}
10671080
}
10681081

1069-
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
1082+
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
1083+
if !blocking {
1084+
throw FileSystemError(.unsupported, path)
1085+
}
1086+
10701087
let resolvedPath: AbsolutePath = try lock.withLock {
10711088
if case let .symlink(destination) = try getNode(path)?.contents {
10721089
return try AbsolutePath(validating: destination, relativeTo: path.parentDirectory)
@@ -1242,8 +1259,8 @@ public final class RerootedFileSystemView: FileSystem {
12421259
try underlyingFileSystem.move(from: formUnderlyingPath(sourcePath), to: formUnderlyingPath(sourcePath))
12431260
}
12441261

1245-
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType = .exclusive, _ body: () throws -> T) throws -> T {
1246-
return try underlyingFileSystem.withLock(on: formUnderlyingPath(path), type: type, body)
1262+
public func withLock<T>(on path: AbsolutePath, type: FileLock.LockType, blocking: Bool, _ body: () throws -> T) throws -> T {
1263+
return try underlyingFileSystem.withLock(on: formUnderlyingPath(path), type: type, blocking: blocking, body)
12471264
}
12481265
}
12491266

Sources/TSCBasic/Lock.swift

+32-25
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ extension NSLock {
4545
}
4646
}
4747

48-
enum ProcessLockError: Error {
48+
public enum ProcessLockError: Error {
4949
case unableToAquireLock(errno: Int32)
5050
}
5151

@@ -89,7 +89,7 @@ public final class FileLock {
8989
/// Try to acquire a lock. This method will block until lock the already aquired by other process.
9090
///
9191
/// Note: This method can throw if underlying POSIX methods fail.
92-
public func lock(type: LockType = .exclusive) throws {
92+
public func lock(type: LockType = .exclusive, blocking: Bool = true) throws {
9393
#if os(Windows)
9494
if handle == nil {
9595
let h: HANDLE = lockFile.pathString.withCString(encodedAs: UTF16.self, {
@@ -112,17 +112,17 @@ public final class FileLock {
112112
overlapped.Offset = 0
113113
overlapped.OffsetHigh = 0
114114
overlapped.hEvent = nil
115+
var dwFlags = Int32(0)
115116
switch type {
116-
case .exclusive:
117-
if !LockFileEx(handle, DWORD(LOCKFILE_EXCLUSIVE_LOCK), 0,
118-
UInt32.max, UInt32.max, &overlapped) {
119-
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
120-
}
121-
case .shared:
122-
if !LockFileEx(handle, 0, 0,
123-
UInt32.max, UInt32.max, &overlapped) {
124-
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
125-
}
117+
case .exclusive: dwFlags |= LOCKFILE_EXCLUSIVE_LOCK
118+
case .shared: break
119+
}
120+
if !blocking {
121+
dwFlags |= LOCKFILE_FAIL_IMMEDIATELY
122+
}
123+
if !LockFileEx(handle, DWORD(dwFlags), 0,
124+
UInt32.max, UInt32.max, &overlapped) {
125+
throw ProcessLockError.unableToAquireLock(errno: Int32(GetLastError()))
126126
}
127127
#else
128128
// Open the lock file.
@@ -133,11 +133,17 @@ public final class FileLock {
133133
}
134134
self.fileDescriptor = fd
135135
}
136+
var flags = Int32(0)
137+
switch type {
138+
case .exclusive: flags = LOCK_EX
139+
case .shared: flags = LOCK_SH
140+
}
141+
if !blocking {
142+
flags |= LOCK_NB
143+
}
136144
// Aquire lock on the file.
137145
while true {
138-
if type == .exclusive && flock(fileDescriptor!, LOCK_EX) == 0 {
139-
break
140-
} else if type == .shared && flock(fileDescriptor!, LOCK_SH) == 0 {
146+
if flock(fileDescriptor!, flags) == 0 {
141147
break
142148
}
143149
// Retry if interrupted.
@@ -172,23 +178,22 @@ public final class FileLock {
172178
}
173179

174180
/// Execute the given block while holding the lock.
175-
public func withLock<T>(type: LockType = .exclusive, _ body: () throws -> T) throws -> T {
176-
try lock(type: type)
181+
public func withLock<T>(type: LockType = .exclusive, blocking: Bool = true, _ body: () throws -> T) throws -> T {
182+
try lock(type: type, blocking: blocking)
177183
defer { unlock() }
178184
return try body()
179185
}
180186

181187
/// Execute the given block while holding the lock.
182-
public func withLock<T>(type: LockType = .exclusive, _ body: () async throws -> T) async throws -> T {
183-
try lock(type: type)
188+
public func withLock<T>(type: LockType = .exclusive, blocking: Bool = true, _ body: () async throws -> T) async throws -> T {
189+
try lock(type: type, blocking: blocking)
184190
defer { unlock() }
185191
return try await body()
186192
}
187193

188194
private static func prepareLock(
189195
fileToLock: AbsolutePath,
190-
at lockFilesDirectory: AbsolutePath? = nil,
191-
_ type: LockType = .exclusive
196+
at lockFilesDirectory: AbsolutePath? = nil
192197
) throws -> FileLock {
193198
// unless specified, we use the tempDirectory to store lock files
194199
let lockFilesDirectory = try lockFilesDirectory ?? localFileSystem.tempDirectory
@@ -233,19 +238,21 @@ public final class FileLock {
233238
fileToLock: AbsolutePath,
234239
lockFilesDirectory: AbsolutePath? = nil,
235240
type: LockType = .exclusive,
241+
blocking: Bool = true,
236242
body: () throws -> T
237243
) throws -> T {
238-
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory, type)
239-
return try lock.withLock(type: type, body)
244+
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory)
245+
return try lock.withLock(type: type, blocking: blocking, body)
240246
}
241247

242248
public static func withLock<T>(
243249
fileToLock: AbsolutePath,
244250
lockFilesDirectory: AbsolutePath? = nil,
245251
type: LockType = .exclusive,
252+
blocking: Bool = true,
246253
body: () async throws -> T
247254
) async throws -> T {
248-
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory, type)
249-
return try await lock.withLock(type: type, body)
255+
let lock = try Self.prepareLock(fileToLock: fileToLock, at: lockFilesDirectory)
256+
return try await lock.withLock(type: type, blocking: blocking, body)
250257
}
251258
}

0 commit comments

Comments
 (0)