1
1
import Foundation
2
2
import SystemPackage
3
3
4
- /**
5
- * A non-blocking file lock implementation using file creation as locking mechanism.
6
- * Use case: When installing multiple Swiftly instances on the same machine,
7
- * one should acquire the lock while others poll until it becomes available.
8
- */
4
+ enum FileLockError : Error {
5
+ case cannotAcquireLock
6
+ case timeoutExceeded
7
+ }
9
8
10
- public actor FileLock {
9
+ /// A non-blocking file lock implementation using file creation as locking mechanism.
10
+ /// Use case: When installing multiple Swiftly instances on the same machine,
11
+ /// one should acquire the lock while others poll until it becomes available.
12
+ public struct FileLock {
11
13
let filePath : FilePath
12
- private var isLocked = false
13
14
14
15
public static let defaultPollingInterval : TimeInterval = 1
15
16
public static let defaultTimeout : TimeInterval = 300.0
16
17
17
- public init ( at path: FilePath ) {
18
+ public init ( at path: FilePath ) throws {
18
19
self . filePath = path
19
- }
20
-
21
- public func tryLock( ) async -> Bool {
22
20
do {
23
- guard !self . isLocked else { return true }
24
-
25
- guard !( try await FileSystem . exists ( atPath: self . filePath) ) else {
26
- return false
27
- }
28
- // Create the lock file with exclusive permissions
29
- try await FileSystem . create ( . mode( 0o600 ) , file: self . filePath, contents: nil )
30
- self . isLocked = true
31
- return true
32
- } catch {
33
- return false
21
+ let fileURL = URL ( fileURLWithPath: self . filePath. string)
22
+ let contents = getpid ( ) . description. data ( using: . utf8) ?? Data ( )
23
+ try contents. write ( to: fileURL, options: . withoutOverwriting)
24
+ } catch CocoaError . fileWriteFileExists {
25
+ throw FileLockError . cannotAcquireLock
34
26
}
35
27
}
36
28
37
- public func waitForLock(
29
+ public static func waitForLock(
30
+ _ path: FilePath ,
38
31
timeout: TimeInterval = FileLock . defaultTimeout,
39
32
pollingInterval: TimeInterval = FileLock . defaultPollingInterval
40
- ) async -> Bool {
33
+ ) async throws -> FileLock {
41
34
let start = Date ( )
42
-
43
35
while Date ( ) . timeIntervalSince ( start) < timeout {
44
- if await self . tryLock ( ) {
45
- return true
36
+ if let fileLock = try ? FileLock ( at : path ) {
37
+ return fileLock
46
38
}
47
39
try ? await Task . sleep ( for: . seconds( pollingInterval) )
48
40
}
49
41
50
- return false
42
+ throw FileLockError . timeoutExceeded
51
43
}
52
44
53
45
public func unlock( ) async throws {
54
- guard self . isLocked else { return }
55
-
56
46
try await FileSystem . remove ( atPath: self . filePath)
57
- self . isLocked = false
58
47
}
59
48
}
60
49
@@ -64,20 +53,22 @@ public func withLock<T>(
64
53
pollingInterval: TimeInterval = FileLock . defaultPollingInterval,
65
54
action: @escaping ( ) async throws -> T
66
55
) async throws -> T {
67
- let lock = FileLock ( at: lockFile)
68
- guard await lock. waitForLock ( timeout: timeout, pollingInterval: pollingInterval) else {
69
- throw SwiftlyError ( message: " Failed to acquire file lock at \( lock. filePath) " )
56
+ guard
57
+ let lock = try ? await FileLock . waitForLock (
58
+ lockFile,
59
+ timeout: timeout,
60
+ pollingInterval: pollingInterval
61
+ )
62
+ else {
63
+ throw SwiftlyError ( message: " Failed to acquire file lock at \( lockFile) " )
70
64
}
71
65
72
- defer {
73
- Task {
74
- do {
75
- try await lock. unlock ( )
76
- } catch {
77
- print ( " WARNING: Failed to unlock file: \( error) " )
78
- }
79
- }
66
+ do {
67
+ let result = try await action ( )
68
+ try await lock. unlock ( )
69
+ return result
70
+ } catch {
71
+ try await lock. unlock ( )
72
+ throw error
80
73
}
81
-
82
- return try await action ( )
83
74
}
0 commit comments