11import Foundation
22import SystemPackage
33
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+ }
98
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 {
1113 let filePath : FilePath
12- private var isLocked = false
1314
1415 public static let defaultPollingInterval : TimeInterval = 1
1516 public static let defaultTimeout : TimeInterval = 300.0
1617
17- public init ( at path: FilePath ) {
18+ public init ( at path: FilePath ) throws {
1819 self . filePath = path
19- }
20-
21- public func tryLock( ) async -> Bool {
2220 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 = Foundation . ProcessInfo. processInfo. processIdentifier. description. data ( using: . utf8) ?? Data ( )
23+ try contents. write ( to: fileURL, options: . withoutOverwriting)
24+ } catch CocoaError . fileWriteFileExists {
25+ throw FileLockError . cannotAcquireLock
3426 }
3527 }
3628
37- public func waitForLock(
29+ public static func waitForLock(
30+ _ path: FilePath ,
3831 timeout: TimeInterval = FileLock . defaultTimeout,
3932 pollingInterval: TimeInterval = FileLock . defaultPollingInterval
40- ) async -> Bool {
33+ ) async throws -> FileLock {
4134 let start = Date ( )
42-
4335 while Date ( ) . timeIntervalSince ( start) < timeout {
44- if await self . tryLock ( ) {
45- return true
36+ if let fileLock = try ? FileLock ( at : path ) {
37+ return fileLock
4638 }
4739 try ? await Task . sleep ( for: . seconds( pollingInterval) )
4840 }
4941
50- return false
42+ throw FileLockError . timeoutExceeded
5143 }
5244
5345 public func unlock( ) async throws {
54- guard self . isLocked else { return }
55-
5646 try await FileSystem . remove ( atPath: self . filePath)
57- self . isLocked = false
5847 }
5948}
6049
@@ -64,20 +53,22 @@ public func withLock<T>(
6453 pollingInterval: TimeInterval = FileLock . defaultPollingInterval,
6554 action: @escaping ( ) async throws -> T
6655) 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) " )
7064 }
7165
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
8073 }
81-
82- return try await action ( )
8374}
0 commit comments