Skip to content

Commit eb5dd59

Browse files
committed
Add advisory file locking API
1 parent 252c0f0 commit eb5dd59

File tree

4 files changed

+147
-1
lines changed

4 files changed

+147
-1
lines changed

Sources/System/FileLock.swift

+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
#if !os(Windows)
2+
extension FileDescriptor {
3+
/// Apply an advisory lock to the file associated with this descriptor.
4+
///
5+
/// Advisory locks allow cooperating processes to perform consistent operations on files,
6+
/// but do not guarantee consistency (i.e., processes may still access files without using advisory locks
7+
/// possibly resulting in inconsistencies).
8+
///
9+
/// The locking mechanism allows two types of locks: shared locks and exclusive locks.
10+
/// At any time multiple shared locks may be applied to a file, but at no time are multiple exclusive, or
11+
/// both shared and exclusive, locks allowed simultaneously on a file.
12+
///
13+
/// A shared lock may be upgraded to an exclusive lock, and vice versa, simply by specifying the appropriate
14+
/// lock type; this results in the previous lock being released and the new lock
15+
/// applied (possibly after other processes have gained and released the lock).
16+
///
17+
/// Requesting a lock on an object that is already locked normally causes the caller to be blocked
18+
/// until the lock may be acquired. If `nonBlocking` is passed as true, then this will not
19+
/// happen; instead the call will fail and `Errno.wouldBlock` will be thrown.
20+
///
21+
/// Locks are on files, not file descriptors. That is, file descriptors duplicated through `FileDescriptor.duplicate`
22+
/// do not result in multiple instances of a lock, but rather multiple references to a
23+
/// single lock. If a process holding a lock on a file forks and the child explicitly unlocks the file, the parent will lose its lock.
24+
///
25+
/// The corresponding C function is `flock()`
26+
@_alwaysEmitIntoClient
27+
public func lock(
28+
exclusive: Bool = false,
29+
nonBlocking: Bool = false,
30+
retryOnInterrupt: Bool = true
31+
) throws {
32+
try _lock(exclusive: exclusive, nonBlocking: nonBlocking, retryOnInterrupt: retryOnInterrupt).get()
33+
}
34+
35+
/// Unlocks an existing advisory lock on the file associated with this descriptor.
36+
///
37+
/// The corresponding C function is `flock` passed `LOCK_UN`
38+
@_alwaysEmitIntoClient
39+
public func unlock(retryOnInterrupt: Bool = true) throws {
40+
try _unlock(retryOnInterrupt: retryOnInterrupt).get()
41+
42+
}
43+
44+
@usableFromInline
45+
internal func _lock(
46+
exclusive: Bool,
47+
nonBlocking: Bool,
48+
retryOnInterrupt: Bool
49+
) -> Result<(), Errno> {
50+
var operation: CInt
51+
if exclusive {
52+
operation = _LOCK_EX
53+
} else {
54+
operation = _LOCK_SH
55+
}
56+
if nonBlocking {
57+
operation |= _LOCK_NB
58+
}
59+
return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) {
60+
system_flock(self.rawValue, operation)
61+
}
62+
}
63+
64+
@usableFromInline
65+
internal func _unlock(
66+
retryOnInterrupt: Bool
67+
) -> Result<(), Errno> {
68+
return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) {
69+
system_flock(self.rawValue, _LOCK_UN)
70+
}
71+
}
72+
}
73+
#endif
74+

Sources/System/Internals/Constants.swift

+13
Original file line numberDiff line numberDiff line change
@@ -528,3 +528,16 @@ internal var _SEEK_HOLE: CInt { SEEK_HOLE }
528528
internal var _SEEK_DATA: CInt { SEEK_DATA }
529529
#endif
530530

531+
#if !os(Windows)
532+
@_alwaysEmitIntoClient
533+
internal var _LOCK_SH: CInt { LOCK_SH }
534+
535+
@_alwaysEmitIntoClient
536+
internal var _LOCK_EX: CInt { LOCK_EX }
537+
538+
@_alwaysEmitIntoClient
539+
internal var _LOCK_NB: CInt { LOCK_NB }
540+
541+
@_alwaysEmitIntoClient
542+
internal var _LOCK_UN: CInt { LOCK_UN }
543+
#endif

Sources/System/Internals/Syscalls.swift

+34
Original file line numberDiff line numberDiff line change
@@ -133,3 +133,37 @@ internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 {
133133
return ftruncate(fd, length)
134134
}
135135
#endif
136+
137+
#if !os(Windows)
138+
internal func system_flock(_ fd: Int32, _ operation: Int32) -> Int32 {
139+
#if ENABLE_MOCKING
140+
if mockingEnabled { return _mock(fd, operation) }
141+
#endif
142+
return flock(fd, operation)
143+
}
144+
#endif
145+
146+
#if !os(Windows)
147+
internal func system_fcntl(_ fd: Int32, _ cmd: Int32) -> Int32 {
148+
#if ENABLE_MOCKING
149+
if mockingEnabled { return _mock(fd, cmd) }
150+
#endif
151+
return fcntl(fd, cmd)
152+
}
153+
154+
internal func system_fcntl(_ fd: Int32, _ cmd: Int32, _ arg: Int32) -> Int32 {
155+
#if ENABLE_MOCKING
156+
if mockingEnabled { return _mock(fd, cmd, arg) }
157+
#endif
158+
return fcntl(fd, cmd, arg)
159+
}
160+
161+
internal func system_fcntl(
162+
_ fd: Int32, _ cmd: Int32, _ arg: UnsafeMutableRawPointer
163+
) -> Int32 {
164+
#if ENABLE_MOCKING
165+
if mockingEnabled { return _mock(fd, cmd, arg) }
166+
#endif
167+
return fcntl(fd, cmd, arg)
168+
}
169+
#endif

Tests/SystemTests/FileOperationsTest.swift

+26-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ final class FileOperationsTest: XCTestCase {
2828
let writeBuf = UnsafeRawBufferPointer(rawBuf)
2929
let writeBufAddr = writeBuf.baseAddress
3030

31-
let syscallTestCases: Array<MockTestCase> = [
31+
var syscallTestCases: Array<MockTestCase> = [
3232
MockTestCase(name: "open", .interruptable, "a path", O_RDWR | O_APPEND) {
3333
retryOnInterrupt in
3434
_ = try FileDescriptor.open(
@@ -83,6 +83,27 @@ final class FileOperationsTest: XCTestCase {
8383
},
8484
]
8585

86+
#if !os(Windows)
87+
syscallTestCases.append(contentsOf: [
88+
// flock
89+
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_SH) { retryOnInterrupt in
90+
_ = try fd.lock(exclusive: false, nonBlocking: false, retryOnInterrupt: retryOnInterrupt)
91+
},
92+
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_SH | LOCK_NB) { retryOnInterrupt in
93+
_ = try fd.lock(exclusive: false, nonBlocking: true, retryOnInterrupt: retryOnInterrupt)
94+
},
95+
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_EX) { retryOnInterrupt in
96+
_ = try fd.lock(exclusive: true, nonBlocking: false, retryOnInterrupt: retryOnInterrupt)
97+
},
98+
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_EX | LOCK_NB) { retryOnInterrupt in
99+
_ = try fd.lock(exclusive: true, nonBlocking: true, retryOnInterrupt: retryOnInterrupt)
100+
},
101+
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_UN) { retryOnInterrupt in
102+
_ = try fd.unlock(retryOnInterrupt: retryOnInterrupt)
103+
},
104+
])
105+
#endif
106+
86107
for test in syscallTestCases { test.runAllTests() }
87108
}
88109

@@ -203,6 +224,10 @@ final class FileOperationsTest: XCTestCase {
203224
XCTAssertEqual(readBytesAfterTruncation, Array("ab".utf8))
204225
}
205226
}
227+
228+
func testFlock() throws {
229+
// TODO: We need multiple processes in order to test blocking behavior
230+
}
206231
#endif
207232
}
208233

0 commit comments

Comments
 (0)