Skip to content

Commit b410967

Browse files
committed
Use atomic create for lock
1 parent a7399d1 commit b410967

File tree

2 files changed

+36
-45
lines changed

2 files changed

+36
-45
lines changed

Sources/Swiftly/Install.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ struct Install: SwiftlyCommand {
357357

358358
let (pathChanged, newConfig) = try await withLock(lockFile) {
359359
if verbose {
360-
await ctx.print("Acquired installation lock.")
360+
await ctx.print("Acquired installation lock")
361361
}
362362

363363
var config = try await Config.load(ctx)

Sources/SwiftlyCore/FileLock.swift

Lines changed: 35 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,60 +1,49 @@
11
import Foundation
22
import 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 = getpid().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

Comments
 (0)