From 6a4dcd049364b24dac78acf5b6f9af9462f0aa59 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Wed, 12 Oct 2022 10:06:28 -0600 Subject: [PATCH] Add advisory file locking API --- Sources/System/FileLock.swift | 74 ++++++++++++++++++++++ Sources/System/Internals/Constants.swift | 13 ++++ Sources/System/Internals/Syscalls.swift | 34 ++++++++++ Tests/SystemTests/FileOperationsTest.swift | 23 +++++++ 4 files changed, 144 insertions(+) create mode 100644 Sources/System/FileLock.swift diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift new file mode 100644 index 00000000..91297e55 --- /dev/null +++ b/Sources/System/FileLock.swift @@ -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 + diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 5489a550..ec7a6f02 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -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 diff --git a/Sources/System/Internals/Syscalls.swift b/Sources/System/Internals/Syscalls.swift index b22d6a3f..e681da20 100644 --- a/Sources/System/Internals/Syscalls.swift +++ b/Sources/System/Internals/Syscalls.swift @@ -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 diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 419e1c97..d11bb859 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -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() } @@ -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 }