Skip to content

Commit c2ccb54

Browse files
committed
lock/tryLock/unlock
1 parent d05c4b9 commit c2ccb54

File tree

3 files changed

+175
-145
lines changed

3 files changed

+175
-145
lines changed

Sources/System/FileLock.swift

+119-68
Original file line numberDiff line numberDiff line change
@@ -127,11 +127,11 @@ extension FileDescriptor.FileLock {
127127
}
128128

129129
extension FileDescriptor {
130-
/// All bytes in a file
131-
@_alwaysEmitIntoClient
132-
internal var _allFileBytes: Range<Int64> { Int64.min ..< Int64.max }
133-
134-
/// Get any conflicting locks held by other open file descriptions.
130+
/// Set an advisory open file description lock.
131+
///
132+
/// If the open file description already has a lock, the old lock is
133+
/// replaced. If the lock cannot be set because it is blocked by an existing lock,
134+
/// this will wait until the lock can be set.
135135
///
136136
/// Open file description locks are associated with an open file
137137
/// description (see `FileDescriptor.open`). Duplicated
@@ -146,69 +146,92 @@ extension FileDescriptor {
146146
/// Open file description locks are inherited by child processes across
147147
/// `fork`, etc.
148148
///
149+
/// Passing a lock kind of `.none` will remove a lock (equivalent to calling
150+
/// `FileDescriptor.unlock()`).
151+
///
149152
/// - Parameters:
150-
/// - byteRange: The range of bytes over which to check for a lock. Pass
153+
/// - kind: The kind of lock to set
154+
/// - byteRange: The range of bytes over which to lock. Pass
151155
/// `nil` to consider the entire file.
152156
/// - retryOnInterrupt: Whether to retry the operation if it throws
153157
/// ``Errno/interrupted``. The default is `true`. Pass `false` to try
154158
/// only once and throw an error upon interruption.
155-
/// - Returns; `.none` if there are no locks, otherwise returns the
156-
/// strongest conflicting lock
157159
///
158-
/// The corresponding C function is `fcntl` with `F_OFD_GETLK`.
160+
/// The corresponding C function is `fcntl` with `F_OFD_SETLKW`.
159161
@_alwaysEmitIntoClient
160-
public func getConflictingLock(
162+
public func lock(
163+
_ kind: FileDescriptor.FileLock.Kind = .read,
161164
byteRange: (some RangeExpression<Int64>)? = Range?.none,
162165
retryOnInterrupt: Bool = true
163-
) throws -> FileDescriptor.FileLock.Kind {
166+
) throws {
164167
let (start, len) = _mapByteRangeToByteOffsets(byteRange)
165-
return try _getConflictingLock(
166-
start: start, length: len, retryOnInterrupt: retryOnInterrupt
168+
try _lock(
169+
kind,
170+
start: start,
171+
length: len,
172+
retryOnInterrupt: retryOnInterrupt
167173
).get()
168174
}
169175

170-
@usableFromInline
171-
internal func _getConflictingLock(
172-
start: Int64, length: Int64, retryOnInterrupt: Bool
173-
) -> Result<FileDescriptor.FileLock.Kind, Errno> {
174-
// If there are multiple locks already in place on a file region, the lock that
175-
// is returned is unspecified. E.g. there could be a write lock over one
176-
// portion of the file and a read lock over another overlapping
177-
// region. Thus, we first check if there are any write locks, and if not
178-
// we issue another call to check for any reads-or-writes.
179-
//
180-
// 1) Try with a read lock, which will tell us if there's a conflicting
181-
// write lock in place.
182-
//
183-
// 2) Try with a write lock, which will tell us if there's either a
184-
// conflicting read or write lock in place.
185-
var lock = FileDescriptor.FileLock(ofdType: .read, start: start, length: length)
186-
if case let .failure(err) = self._fcntl(
187-
.getOFDLock, &lock, retryOnInterrupt: retryOnInterrupt
188-
) {
189-
return .failure(err)
190-
}
191-
if lock.type == .write {
192-
return .success(.write)
193-
}
194-
guard lock.type == .none else {
195-
fatalError("FIXME: really shouldn't be possible")
176+
/// Try to set an advisory open file description lock.
177+
///
178+
/// If the open file description already has a lock, the old lock is
179+
/// replaced. If the lock cannot be set because it is blocked by an existing lock,
180+
/// that is if the syscall would throw `.resourceTemporarilyUnavailable`
181+
/// (aka `EAGAIN`), this will return `false`.
182+
///
183+
/// Open file description locks are associated with an open file
184+
/// description (see `FileDescriptor.open`). Duplicated
185+
/// file descriptors (see `FileDescriptor.duplicate`) share open file
186+
/// description locks.
187+
///
188+
/// Locks are advisory, which allow cooperating code to perform
189+
/// consistent operations on files, but do not guarantee consistency.
190+
/// (i.e. other code may still access files without using advisory locks
191+
/// possibly resulting in inconsistencies).
192+
///
193+
/// Open file description locks are inherited by child processes across
194+
/// `fork`, etc.
195+
///
196+
/// Passing a lock kind of `.none` will remove a lock (equivalent to calling
197+
/// `FileDescriptor.unlock()`).
198+
///
199+
/// - Parameters:
200+
/// - kind: The kind of lock to set
201+
/// - byteRange: The range of bytes over which to lock. Pass
202+
/// `nil` to consider the entire file.
203+
/// - retryOnInterrupt: Whether to retry the operation if it throws
204+
/// ``Errno/interrupted``. The default is `true`. Pass `false` to try
205+
/// only once and throw an error upon interruption.
206+
/// - Returns: `true` if the lock was aquired, `false` otherwise
207+
///
208+
/// The corresponding C function is `fcntl` with `F_OFD_SETLK`.
209+
@_alwaysEmitIntoClient
210+
public func tryLock(
211+
_ kind: FileDescriptor.FileLock.Kind = .read,
212+
byteRange: (some RangeExpression<Int64>)? = Range?.none,
213+
retryOnInterrupt: Bool = true
214+
) throws -> Bool {
215+
let (start, len) = _mapByteRangeToByteOffsets(byteRange)
216+
guard let _ = try _tryLock(
217+
kind,
218+
waitUntilTimeout: false,
219+
start: start,
220+
length: len,
221+
retryOnInterrupt: retryOnInterrupt
222+
)?.get() else {
223+
return false
196224
}
197-
// This means there was no conflicting lock, so try to detect reads
198-
lock = FileDescriptor.FileLock(ofdType: .write, start: start, length: length)
199-
200-
let secondTry = self._fcntl(.getOFDLock, &lock, retryOnInterrupt: retryOnInterrupt)
201-
return secondTry.map { lock.type }
225+
return true
202226
}
203227

204-
/// Set an open file description lock.
228+
#if !os(Linux)
229+
/// Try to set an advisory open file description lock.
205230
///
206231
/// If the open file description already has a lock, the old lock is
207-
/// replaced.
208-
///
209-
/// If the lock cannot be set because it is blocked by an existing lock on a
210-
/// file and `wait` is `false`,
211-
/// `Errno.resourceTemporarilyUnavailable` is thrown.
232+
/// replaced. If the lock cannot be set because it is blocked by an existing lock,
233+
/// that is if the syscall would throw `.resourceTemporarilyUnavailable`
234+
/// (aka `EAGAIN`), this will return `false`.
212235
///
213236
/// Open file description locks are associated with an open file
214237
/// description (see `FileDescriptor.open`). Duplicated
@@ -230,29 +253,33 @@ extension FileDescriptor {
230253
/// - kind: The kind of lock to set
231254
/// - byteRange: The range of bytes over which to lock. Pass
232255
/// `nil` to consider the entire file.
233-
/// - wait: Whether to wait (block) until the request can be completed
256+
/// - waitUntilTimeout: If `true`, will wait until a timeout (determined by the operating system)
234257
/// - retryOnInterrupt: Whether to retry the operation if it throws
235258
/// ``Errno/interrupted``. The default is `true`. Pass `false` to try
236259
/// only once and throw an error upon interruption.
260+
/// - Returns: `true` if the lock was aquired, `false` otherwise
237261
///
238-
/// The corresponding C function is `fcntl` with `F_OFD_SETLK` or
239-
/// `F_OFD_SETLKW`.
262+
/// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_OFD_SETLKWTIMEOUT` .
240263
@_alwaysEmitIntoClient
241-
public func lock(
264+
public func tryLock(
242265
_ kind: FileDescriptor.FileLock.Kind = .read,
243266
byteRange: (some RangeExpression<Int64>)? = Range?.none,
244-
wait: Bool = false,
267+
waitUntilTimeout: Bool,
245268
retryOnInterrupt: Bool = true
246-
) throws {
269+
) throws -> Bool {
247270
let (start, len) = _mapByteRangeToByteOffsets(byteRange)
248-
try _lock(
271+
guard let _ = try _tryLock(
249272
kind,
273+
waitUntilTimeout: waitUntilTimeout,
250274
start: start,
251275
length: len,
252-
wait: wait,
253276
retryOnInterrupt: retryOnInterrupt
254-
).get()
277+
)?.get() else {
278+
return false
279+
}
280+
return true
255281
}
282+
#endif
256283

257284
/// Remove an open file description lock.
258285
///
@@ -275,7 +302,6 @@ extension FileDescriptor {
275302
/// - Parameters:
276303
/// - byteRange: The range of bytes over which to lock. Pass
277304
/// `nil` to consider the entire file.
278-
/// - wait: Whether to wait (block) until the request can be completed
279305
/// - retryOnInterrupt: Whether to retry the operation if it throws
280306
/// ``Errno/interrupted``. The default is `true`. Pass `false` to try
281307
/// only once and throw an error upon interruption.
@@ -289,27 +315,52 @@ extension FileDescriptor {
289315
retryOnInterrupt: Bool = true
290316
) throws {
291317
let (start, len) = _mapByteRangeToByteOffsets(byteRange)
292-
try _lock(
318+
guard let res = _tryLock(
293319
.none,
320+
waitUntilTimeout: false, // TODO: or we wait for timeout?
294321
start: start,
295322
length: len,
296-
wait: wait,
297323
retryOnInterrupt: retryOnInterrupt
298-
).get()
324+
) else {
325+
preconditionFailure("TODO: Unlock should always succeed?")
326+
}
327+
return try res.get()
299328
}
300329

301330
@usableFromInline
302331
internal func _lock(
303332
_ kind: FileDescriptor.FileLock.Kind,
304333
start: Int64,
305334
length: Int64,
306-
wait: Bool,
307335
retryOnInterrupt: Bool
308336
) -> Result<(), Errno> {
309-
var lock = FileDescriptor.FileLock(ofdType: kind, start: start, length: length)
310-
let command: FileDescriptor.Control.Command =
311-
wait ? .setOFDLockWait : .setOFDLock
312-
return _fcntl(command, &lock, retryOnInterrupt: retryOnInterrupt)
337+
var lock = FileDescriptor.FileLock(
338+
ofdType: kind, start: start, length: length)
339+
return _fcntl(.setOFDLockWait, &lock, retryOnInterrupt: retryOnInterrupt)
340+
}
341+
342+
@usableFromInline
343+
internal func _tryLock(
344+
_ kind: FileDescriptor.FileLock.Kind,
345+
waitUntilTimeout: Bool,
346+
start: Int64,
347+
length: Int64,
348+
retryOnInterrupt: Bool
349+
) -> Result<(), Errno>? {
350+
#if os(Linux)
351+
precondition(!waitUntilTimeout, "`waitUntilTimeout` unavailable on Linux")
352+
#endif
353+
354+
let cmd: Control.Command
355+
if waitUntilTimeout {
356+
cmd = .setOFDLockWaitTimout
357+
} else {
358+
cmd = .setOFDLock
359+
}
360+
var lock = FileDescriptor.FileLock(
361+
ofdType: kind, start: start, length: length)
362+
return _extractWouldBlock(
363+
_fcntl(cmd, &lock, retryOnInterrupt: retryOnInterrupt))
313364
}
314365
}
315366
#endif

Sources/System/Util.swift

+10
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,16 @@ internal func nothingOrErrno<I: FixedWidthInteger>(
4343
valueOrErrno(retryOnInterrupt: retryOnInterrupt, f).map { _ in () }
4444
}
4545

46+
/// Promote `Errno.wouldBlcok` to `nil`.
47+
internal func _extractWouldBlock<T>(
48+
_ value: Result<T, Errno>
49+
) -> Result<T, Errno>? {
50+
if case .failure(let err) = value, err == .wouldBlock {
51+
return nil
52+
}
53+
return value
54+
}
55+
4656
// Run a precondition for debug client builds
4757
internal func _debugPrecondition(
4858
_ condition: @autoclosure () -> Bool,

0 commit comments

Comments
 (0)