Skip to content

Commit

Permalink
Add advisory file locking API
Browse files Browse the repository at this point in the history
  • Loading branch information
milseman committed Oct 12, 2022
1 parent 252c0f0 commit 6a4dcd0
Show file tree
Hide file tree
Showing 4 changed files with 144 additions and 0 deletions.
74 changes: 74 additions & 0 deletions Sources/System/FileLock.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
#if !os(Windows)
extension FileDescriptor {
/// Apply an advisory lock to the file associated with this descriptor.
///
/// Advisory locks allow cooperating processes to perform consistent operations on files,
/// but do not guarantee consistency (i.e., processes may still access files without using advisory locks
/// possibly resulting in inconsistencies).
///
/// The locking mechanism allows two types of locks: shared locks and exclusive locks.
/// At any time multiple shared locks may be applied to a file, but at no time are multiple exclusive, or
/// both shared and exclusive, locks allowed simultaneously on a file.
///
/// A shared lock may be upgraded to an exclusive lock, and vice versa, simply by specifying the appropriate
/// lock type; this results in the previous lock being released and the new lock
/// applied (possibly after other processes have gained and released the lock).
///
/// Requesting a lock on an object that is already locked normally causes the caller to be blocked
/// until the lock may be acquired. If `nonBlocking` is passed as true, then this will not
/// happen; instead the call will fail and `Errno.wouldBlock` will be thrown.
///
/// Locks are on files, not file descriptors. That is, file descriptors duplicated through `FileDescriptor.duplicate`
/// do not result in multiple instances of a lock, but rather multiple references to a
/// 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.
///
/// The corresponding C function is `flock()`
@_alwaysEmitIntoClient
public func lock(
exclusive: Bool = false,
nonBlocking: Bool = false,
retryOnInterrupt: Bool = true
) throws {
try _lock(exclusive: exclusive, nonBlocking: nonBlocking, retryOnInterrupt: retryOnInterrupt).get()
}

/// Unlocks an existing advisory lock on the file associated with this descriptor.
///
/// The corresponding C function is `flock` passed `LOCK_UN`
@_alwaysEmitIntoClient
public func unlock(retryOnInterrupt: Bool = true) throws {
try _unlock(retryOnInterrupt: retryOnInterrupt).get()

}

@usableFromInline
internal func _lock(
exclusive: Bool,
nonBlocking: Bool,
retryOnInterrupt: Bool
) -> Result<(), Errno> {
var operation: CInt
if exclusive {
operation = _LOCK_EX
} else {
operation = _LOCK_SH
}
if nonBlocking {
operation |= _LOCK_NB
}
return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) {
system_flock(self.rawValue, operation)
}
}

@usableFromInline
internal func _unlock(
retryOnInterrupt: Bool
) -> Result<(), Errno> {
return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) {
system_flock(self.rawValue, _LOCK_UN)
}
}
}
#endif

13 changes: 13 additions & 0 deletions Sources/System/Internals/Constants.swift
Original file line number Diff line number Diff line change
Expand Up @@ -528,3 +528,16 @@ internal var _SEEK_HOLE: CInt { SEEK_HOLE }
internal var _SEEK_DATA: CInt { SEEK_DATA }
#endif

#if !os(Windows)
@_alwaysEmitIntoClient
internal var _LOCK_SH: CInt { LOCK_SH }

@_alwaysEmitIntoClient
internal var _LOCK_EX: CInt { LOCK_EX }

@_alwaysEmitIntoClient
internal var _LOCK_NB: CInt { LOCK_NB }

@_alwaysEmitIntoClient
internal var _LOCK_UN: CInt { LOCK_UN }
#endif
34 changes: 34 additions & 0 deletions Sources/System/Internals/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -133,3 +133,37 @@ internal func system_ftruncate(_ fd: Int32, _ length: off_t) -> Int32 {
return ftruncate(fd, length)
}
#endif

#if !os(Windows)
internal func system_flock(_ fd: Int32, _ operation: Int32) -> Int32 {
#if ENABLE_MOCKING
if mockingEnabled { return _mock(fd, operation) }
#endif
return flock(fd, operation)
}
#endif

#if !os(Windows)
internal func system_fcntl(_ fd: Int32, _ cmd: Int32) -> Int32 {
#if ENABLE_MOCKING
if mockingEnabled { return _mock(fd, cmd) }
#endif
return fcntl(fd, cmd)
}

internal func system_fcntl(_ fd: Int32, _ cmd: Int32, _ arg: Int32) -> Int32 {
#if ENABLE_MOCKING
if mockingEnabled { return _mock(fd, cmd, arg) }
#endif
return fcntl(fd, cmd, arg)
}

internal func system_fcntl(
_ fd: Int32, _ cmd: Int32, _ arg: UnsafeMutableRawPointer
) -> Int32 {
#if ENABLE_MOCKING
if mockingEnabled { return _mock(fd, cmd, arg) }
#endif
return fcntl(fd, cmd, arg)
}
#endif
23 changes: 23 additions & 0 deletions Tests/SystemTests/FileOperationsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,25 @@ final class FileOperationsTest: XCTestCase {
_ = try fd.duplicate(as: FileDescriptor(rawValue: 42),
retryOnInterrupt: retryOnInterrupt)
},

#if !os(Windows)
// flock
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_SH) { retryOnInterrupt in
_ = try fd.lock(exclusive: false, nonBlocking: false, retryOnInterrupt: retryOnInterrupt)
},
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_SH | LOCK_NB) { retryOnInterrupt in
_ = try fd.lock(exclusive: false, nonBlocking: true, retryOnInterrupt: retryOnInterrupt)
},
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_EX) { retryOnInterrupt in
_ = try fd.lock(exclusive: true, nonBlocking: false, retryOnInterrupt: retryOnInterrupt)
},
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_EX | LOCK_NB) { retryOnInterrupt in
_ = try fd.lock(exclusive: true, nonBlocking: true, retryOnInterrupt: retryOnInterrupt)
},
MockTestCase(name: "flock", .interruptable, rawFD, LOCK_UN) { retryOnInterrupt in
_ = try fd.unlock(retryOnInterrupt: retryOnInterrupt)
},
#endif
]

for test in syscallTestCases { test.runAllTests() }
Expand Down Expand Up @@ -203,6 +222,10 @@ final class FileOperationsTest: XCTestCase {
XCTAssertEqual(readBytesAfterTruncation, Array("ab".utf8))
}
}

func testFlock() throws {
// TODO: We need multiple processes in order to test blocking behavior
}
#endif
}

0 comments on commit 6a4dcd0

Please sign in to comment.