Skip to content

Commit e55967c

Browse files
committed
Issue #142 Implement non-blocking file lock
1 parent e3c2f0f commit e55967c

File tree

2 files changed

+129
-0
lines changed

2 files changed

+129
-0
lines changed

Sources/Swiftly/Install.swift

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,13 +81,43 @@ struct Install: SwiftlyCommand {
8181
try await self.run(Swiftly.createDefaultContext())
8282
}
8383

84+
private func swiftlyHomeDir(_ ctx: SwiftlyCoreContext) -> FilePath {
85+
Swiftly.currentPlatform.swiftlyHomeDir(ctx)
86+
}
87+
8488
mutating func run(_ ctx: SwiftlyCoreContext) async throws {
8589
let versionUpdateReminder = try await validateSwiftly(ctx)
8690
defer {
8791
versionUpdateReminder()
8892
}
8993
try await validateLinked(ctx)
9094

95+
let lockFile = self.swiftlyHomeDir(ctx) / "swiftly.lock"
96+
let fileLock: SwiftlyCore.FileLock
97+
98+
do {
99+
fileLock = try SwiftlyCore.FileLock(at: lockFile.string)
100+
} catch {
101+
print("ERROR: Failed to create lock file: \(error)")
102+
Foundation.exit(42)
103+
}
104+
105+
defer {
106+
do {
107+
try fileLock.unlock()
108+
} catch {
109+
print("WARNING: Failed to unlock file: \(error)")
110+
}
111+
}
112+
113+
if self.root.verbose {
114+
await ctx.print("Attempting to acquire installation lock...")
115+
}
116+
guard fileLock.waitForLock(timeout: 300, pollingInterval: 2) else {
117+
print("ERROR: Failed to acquire lock on file: \(lockFile.string) after 300 seconds")
118+
Foundation.exit(42)
119+
}
120+
91121
var config = try await Config.load(ctx)
92122
let toolchainVersion = try await Self.determineToolchainVersion(ctx, version: self.version, config: &config)
93123

Sources/SwiftlyCore/FileLock.swift

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
import Foundation
2+
3+
/**
4+
* A non-blocking file lock implementation with polling capability.
5+
* Use case: When installing multiple Swiftly instances on the same machine,
6+
* one should acquire the lock while others poll until it becomes available.
7+
*/
8+
9+
#if os(macOS)
10+
import Darwin.C
11+
#elseif os(Linux)
12+
import Glibc
13+
#endif
14+
15+
public struct FileLock {
16+
let filePath: String
17+
18+
let fileHandle: FileHandle
19+
20+
public static let defaultPollingInterval: TimeInterval = 1.0
21+
22+
public static let defaultTimeout: TimeInterval = 300.0
23+
24+
public init(at filePath: String) throws {
25+
self.filePath = filePath
26+
27+
if !FileManager.default.fileExists(atPath: filePath) {
28+
FileManager.default.createFile(atPath: filePath, contents: nil)
29+
}
30+
31+
self.fileHandle = try FileHandle(forWritingTo: URL(fileURLWithPath: filePath))
32+
}
33+
34+
public func tryLock() -> Bool {
35+
self.fileHandle.tryLockFile()
36+
}
37+
38+
public func waitForLock(
39+
timeout: TimeInterval = FileLock.defaultTimeout,
40+
pollingInterval: TimeInterval = FileLock.defaultPollingInterval
41+
) -> Bool {
42+
let startTime = Date()
43+
44+
if self.tryLock() {
45+
return true
46+
}
47+
48+
while Date().timeIntervalSince(startTime) < timeout {
49+
Thread.sleep(forTimeInterval: pollingInterval)
50+
51+
if self.tryLock() {
52+
return true
53+
}
54+
}
55+
56+
return false
57+
}
58+
59+
public func unlock() throws {
60+
guard self.fileHandle != nil else { return }
61+
try self.fileHandle.unlockFile()
62+
try self.fileHandle.close()
63+
}
64+
}
65+
66+
extension FileHandle {
67+
func tryLockFile() -> Bool {
68+
let fd = self.fileDescriptor
69+
var flock = flock()
70+
flock.l_type = Int16(F_WRLCK)
71+
flock.l_whence = Int16(SEEK_SET)
72+
flock.l_start = 0
73+
flock.l_len = 0
74+
75+
if fcntl(fd, F_SETLK, &flock) == -1 {
76+
if errno == EACCES || errno == EAGAIN {
77+
return false
78+
} else {
79+
fputs("Unexpected lock error: \(String(cString: strerror(errno)))\n", stderr)
80+
return false
81+
}
82+
}
83+
return true
84+
}
85+
86+
func unlockFile() throws {
87+
let fd = self.fileDescriptor
88+
var flock = flock()
89+
flock.l_type = Int16(F_UNLCK)
90+
flock.l_whence = Int16(SEEK_SET)
91+
flock.l_start = 0
92+
flock.l_len = 0
93+
94+
if fcntl(fd, F_SETLK, &flock) == -1 {
95+
throw SwiftlyError(
96+
message: "Failed to unlock file: \(String(cString: strerror(errno)))")
97+
}
98+
}
99+
}

0 commit comments

Comments
 (0)