Skip to content

Commit a1cbeeb

Browse files
committed
lock and lock(wait:)
1 parent c2ccb54 commit a1cbeeb

File tree

7 files changed

+105
-172
lines changed

7 files changed

+105
-172
lines changed

Sources/System/FileControl.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212

1313
extension FileDescriptor {
1414
internal func _fcntl(
15-
_ cmd: Control.Command, _ lock: inout FileDescriptor.FileLock,
15+
_ cmd: Command, _ lock: inout FileDescriptor.FileLock,
1616
retryOnInterrupt: Bool
1717
) -> Result<(), Errno> {
1818
nothingOrErrno(retryOnInterrupt: retryOnInterrupt) {

Sources/System/FileControlRaw.swift

+3-38
Original file line numberDiff line numberDiff line change
@@ -20,75 +20,40 @@ import Glibc
2020

2121
#if !os(Windows)
2222

23-
extension FileDescriptor {
24-
/// A namespace for types and values for `FileDescriptor.control()`, aka `fcntl`.
25-
///
26-
/// TODO: a better name? "Internals", "Raw", "FCNTL"? I feel like a
27-
/// precedent would be useful for sysctl, ioctl, and other grab-bag
28-
/// things. "junk drawer" can be an anti-pattern, but is better than
29-
/// trashing the higher namespace.
30-
// public
31-
internal enum Control {}
32-
33-
}
3423
// - MARK: Commands
3524

36-
extension FileDescriptor.Control {
25+
// TODO: Make below API as part of broader `fcntl` support.
26+
extension FileDescriptor {
3727
/// Commands (and various constants) to pass to `fcntl`.
38-
// @frozen
39-
// public
4028
internal struct Command: RawRepresentable, Hashable {
41-
// @_alwaysEmitIntoClient
42-
// public
4329
internal let rawValue: CInt
4430

45-
// @_alwaysEmitIntoClient
46-
// public
4731
internal init(rawValue: CInt) { self.rawValue = rawValue }
4832

4933
@_alwaysEmitIntoClient
5034
private init(_ raw: CInt) { self.init(rawValue: raw) }
35+
5136
/// Get open file description record locking information.
5237
///
53-
/// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html
54-
/// TODO: reference FileDesciptor.isLocked() or something like that
55-
///
5638
/// The corresponding C constant is `F_GETLK`.
57-
// @_alwaysEmitIntoClient
58-
// public
5939
internal static var getOFDLock: Command { Command(_F_OFD_GETLK) }
6040

6141
/// Set open file description record locking information.
6242
///
63-
/// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html
64-
/// TODO: reference FileDesciptor.lock()
65-
///
6643
/// The corresponding C constant is `F_SETLK`.
67-
// @_alwaysEmitIntoClient
68-
// public
6944
internal static var setOFDLock: Command { Command(_F_OFD_SETLK) }
7045

7146
/// Set open file description record locking information and wait until
7247
/// the request can be completed.
7348
///
74-
/// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html
75-
/// TODO: reference FileDesciptor.lock()
76-
///
7749
/// The corresponding C constant is `F_SETLKW`.
78-
// @_alwaysEmitIntoClient
79-
// public
8050
internal static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) }
8151

8252
#if !os(Linux)
8353
/// Set open file description record locking information and wait until
8454
/// the request can be completed, returning on timeout.
8555
///
86-
/// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html
87-
/// TODO: reference FileDesciptor.lock()
88-
///
8956
/// The corresponding C constant is `F_SETLKWTIMEOUT`.
90-
// @_alwaysEmitIntoClient
91-
// public
9257
internal static var setOFDLockWaitTimout: Command {
9358
Command(_F_OFD_SETLKWTIMEOUT)
9459
}

Sources/System/FileLock.swift

+69-57
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,18 @@ extension FileDescriptor.FileLock {
123123
public static var none: Self {
124124
Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_UNLCK))
125125
}
126+
127+
/// Shared (alias for `read`)
128+
@_alwaysEmitIntoClient
129+
public static var shared: Self { .read }
130+
131+
/// Exclusive (alias for `write`)
132+
@_alwaysEmitIntoClient
133+
public static var exclusive: Self { .write }
134+
135+
/// Unlock (alias for `none`)
136+
@_alwaysEmitIntoClient
137+
public static var unlock: Self { .none }
126138
}
127139
}
128140

@@ -131,7 +143,8 @@ extension FileDescriptor {
131143
///
132144
/// If the open file description already has a lock, the old lock is
133145
/// 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.
146+
/// that is if the syscall would throw `.resourceTemporarilyUnavailable`
147+
/// (aka `EAGAIN`), this will return `false`.
135148
///
136149
/// Open file description locks are associated with an open file
137150
/// description (see `FileDescriptor.open`). Duplicated
@@ -156,29 +169,31 @@ extension FileDescriptor {
156169
/// - retryOnInterrupt: Whether to retry the operation if it throws
157170
/// ``Errno/interrupted``. The default is `true`. Pass `false` to try
158171
/// only once and throw an error upon interruption.
172+
/// - Returns: `true` if the lock was aquired, `false` otherwise
159173
///
160-
/// The corresponding C function is `fcntl` with `F_OFD_SETLKW`.
174+
/// The corresponding C function is `fcntl` with `F_OFD_SETLK`.
161175
@_alwaysEmitIntoClient
162176
public func lock(
163177
_ kind: FileDescriptor.FileLock.Kind = .read,
164178
byteRange: (some RangeExpression<Int64>)? = Range?.none,
165179
retryOnInterrupt: Bool = true
166-
) throws {
180+
) throws -> Bool {
167181
let (start, len) = _mapByteRangeToByteOffsets(byteRange)
168-
try _lock(
182+
return try _lock(
169183
kind,
170184
start: start,
171185
length: len,
186+
wait: false,
187+
waitUntilTimeout: false,
172188
retryOnInterrupt: retryOnInterrupt
173-
).get()
189+
)?.get() != nil
174190
}
175191

176-
/// Try to set an advisory open file description lock.
192+
/// Set an advisory open file description lock.
177193
///
178194
/// 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`.
195+
/// replaced. If the lock cannot be set because it is blocked by an existing lock and
196+
/// `wait` is true, this will wait until the lock can be set, otherwise returns `false`.
182197
///
183198
/// Open file description locks are associated with an open file
184199
/// description (see `FileDescriptor.open`). Duplicated
@@ -200,38 +215,38 @@ extension FileDescriptor {
200215
/// - kind: The kind of lock to set
201216
/// - byteRange: The range of bytes over which to lock. Pass
202217
/// `nil` to consider the entire file.
218+
/// - wait: if `true` will wait until the lock can be set
203219
/// - retryOnInterrupt: Whether to retry the operation if it throws
204220
/// ``Errno/interrupted``. The default is `true`. Pass `false` to try
205221
/// only once and throw an error upon interruption.
206-
/// - Returns: `true` if the lock was aquired, `false` otherwise
207222
///
208-
/// The corresponding C function is `fcntl` with `F_OFD_SETLK`.
223+
/// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_OFD_SETLKW`.
224+
@discardableResult
209225
@_alwaysEmitIntoClient
210-
public func tryLock(
226+
public func lock(
211227
_ kind: FileDescriptor.FileLock.Kind = .read,
212228
byteRange: (some RangeExpression<Int64>)? = Range?.none,
229+
wait: Bool,
213230
retryOnInterrupt: Bool = true
214231
) throws -> Bool {
215232
let (start, len) = _mapByteRangeToByteOffsets(byteRange)
216-
guard let _ = try _tryLock(
233+
return try _lock(
217234
kind,
218-
waitUntilTimeout: false,
219235
start: start,
220236
length: len,
237+
wait: wait,
238+
waitUntilTimeout: false,
221239
retryOnInterrupt: retryOnInterrupt
222-
)?.get() else {
223-
return false
224-
}
225-
return true
240+
)?.get() != nil
226241
}
227242

228-
#if !os(Linux)
229-
/// Try to set an advisory open file description lock.
243+
#if !os(Linux)
244+
/// Set an advisory open file description lock.
230245
///
231246
/// If the open file description already has a lock, the old lock is
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`.
247+
/// replaced. If the lock cannot be set because it is blocked by an existing lock and
248+
/// `waitUntilTimeout` is true, this will wait until the lock can be set (or the operating
249+
/// system's timeout expires), otherwise returns `false`.
235250
///
236251
/// Open file description locks are associated with an open file
237252
/// description (see `FileDescriptor.open`). Duplicated
@@ -253,33 +268,30 @@ extension FileDescriptor {
253268
/// - kind: The kind of lock to set
254269
/// - byteRange: The range of bytes over which to lock. Pass
255270
/// `nil` to consider the entire file.
256-
/// - waitUntilTimeout: If `true`, will wait until a timeout (determined by the operating system)
271+
/// - waitUntilTimeout: if `true` will wait until the lock can be set or a timeout expires
257272
/// - retryOnInterrupt: Whether to retry the operation if it throws
258273
/// ``Errno/interrupted``. The default is `true`. Pass `false` to try
259274
/// only once and throw an error upon interruption.
260-
/// - Returns: `true` if the lock was aquired, `false` otherwise
261275
///
262-
/// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_OFD_SETLKWTIMEOUT` .
276+
/// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_SETLKWTIMEOUT`.
263277
@_alwaysEmitIntoClient
264-
public func tryLock(
278+
public func lock(
265279
_ kind: FileDescriptor.FileLock.Kind = .read,
266280
byteRange: (some RangeExpression<Int64>)? = Range?.none,
267281
waitUntilTimeout: Bool,
268282
retryOnInterrupt: Bool = true
269283
) throws -> Bool {
270284
let (start, len) = _mapByteRangeToByteOffsets(byteRange)
271-
guard let _ = try _tryLock(
285+
return try _lock(
272286
kind,
273-
waitUntilTimeout: waitUntilTimeout,
274287
start: start,
275288
length: len,
289+
wait: false,
290+
waitUntilTimeout: waitUntilTimeout,
276291
retryOnInterrupt: retryOnInterrupt
277-
)?.get() else {
278-
return false
279-
}
280-
return true
292+
)?.get() != nil
281293
}
282-
#endif
294+
#endif
283295

284296
/// Remove an open file description lock.
285297
///
@@ -311,49 +323,48 @@ extension FileDescriptor {
311323
@_alwaysEmitIntoClient
312324
public func unlock(
313325
byteRange: (some RangeExpression<Int64>)? = Range?.none,
314-
wait: Bool = false, // FIXME: needed?
315326
retryOnInterrupt: Bool = true
316327
) throws {
317328
let (start, len) = _mapByteRangeToByteOffsets(byteRange)
318-
guard let res = _tryLock(
329+
guard try _lock(
319330
.none,
320-
waitUntilTimeout: false, // TODO: or we wait for timeout?
321331
start: start,
322332
length: len,
333+
wait: false,
334+
waitUntilTimeout: false,
323335
retryOnInterrupt: retryOnInterrupt
324-
) else {
325-
preconditionFailure("TODO: Unlock should always succeed?")
336+
)?.get() != nil else {
337+
// NOTE: Errno and syscall composition wasn't designed for the modern
338+
// world. Releasing locks should always succeed and never be blocked
339+
// by an existing lock held elsewhere. But there's always a chance
340+
// that some effect (e.g. from NFS) causes `EGAIN` to be thrown for a
341+
// different reason/purpose. Here, in the very unlikely situation
342+
// that we somehow saw it, we convert the `nil` back to the error.
343+
throw Errno.resourceTemporarilyUnavailable
326344
}
327-
return try res.get()
328345
}
329346

347+
/// Internal lock entry point, returns `nil` if blocked by existing lock.
348+
/// Both `wait` and `waitUntilTimeout` cannot both be true (passed as bools to avoid
349+
/// spurious enum in the ABI).
330350
@usableFromInline
331351
internal func _lock(
332352
_ kind: FileDescriptor.FileLock.Kind,
333353
start: Int64,
334354
length: Int64,
335-
retryOnInterrupt: Bool
336-
) -> Result<(), Errno> {
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,
355+
wait: Bool,
345356
waitUntilTimeout: Bool,
346-
start: Int64,
347-
length: Int64,
348357
retryOnInterrupt: Bool
349358
) -> Result<(), Errno>? {
359+
precondition(!wait || !waitUntilTimeout)
360+
let cmd: FileDescriptor.Command
361+
if waitUntilTimeout {
350362
#if os(Linux)
351-
precondition(!waitUntilTimeout, "`waitUntilTimeout` unavailable on Linux")
363+
preconditionFailure("`waitUntilTimeout` unavailable on Linux")
352364
#endif
353-
354-
let cmd: Control.Command
355-
if waitUntilTimeout {
356365
cmd = .setOFDLockWaitTimout
366+
} else if wait {
367+
cmd = .setOFDLockWait
357368
} else {
358369
cmd = .setOFDLock
359370
}
@@ -363,5 +374,6 @@ extension FileDescriptor {
363374
_fcntl(cmd, &lock, retryOnInterrupt: retryOnInterrupt))
364375
}
365376
}
366-
#endif
377+
378+
#endif // !os(Windows)
367379

Sources/System/Internals/CInterop.swift

+1-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,6 @@ public enum CInterop {
7070
#if !os(Windows)
7171
/// The C `struct flock` type
7272
public typealias FileLock = flock
73-
#endif
7473

7574
/// The C `pid_t` type
7675
public typealias PID = pid_t
@@ -81,6 +80,7 @@ public enum CInterop {
8180
/// might otherwise appear. This typealias allows conversion code to be
8281
/// emitted into client.
8382
public typealias Offset = off_t
83+
#endif
8484

8585
}
8686

Sources/System/Util.swift

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

46-
/// Promote `Errno.wouldBlcok` to `nil`.
46+
/// Promote `Errno.wouldBlock` / `Errno.resourceTemporarilyUnavailable` to `nil`.
4747
internal func _extractWouldBlock<T>(
4848
_ value: Result<T, Errno>
4949
) -> Result<T, Errno>? {
@@ -166,32 +166,9 @@ internal func _mapByteRangeToByteOffsets(
166166

167167
let start = br.lowerBound == Int64.min ? 0 : br.lowerBound
168168

169-
let len: Int64 = {
170-
if br.upperBound == Int64.max {
171-
// l_len == 0 means until end of file
172-
return 0
173-
}
174-
return br.upperBound - start
175-
}()
176-
return (start, len)
177-
178-
/*
179-
180-
let len: Int64 = {
181-
if byteRange.upperBound == Int64.max {
182-
// l_len == 0 means until end of file
183-
0
184-
} else {
185-
byteRange.upperBound - start
186-
}
187-
}()
188-
189-
let len = if byteRange.upperbound == Int64.max {
190-
// l_len == 0 means until end of file
191-
0
192-
} else {
193-
byteRange.upperBound - start
194-
}
195-
196-
*/
169+
if br.upperBound == Int64.max {
170+
// l_len == 0 means until end of file
171+
return (start, 0)
172+
}
173+
return (start, br.upperBound - start)
197174
}

0 commit comments

Comments
 (0)