From eb5dd59d800906ecd7fe1a4197ba2f68fdb68a2c Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Wed, 12 Oct 2022 10:06:28 -0600 Subject: [PATCH 01/26] 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 | 27 +++++++- 4 files changed, 147 insertions(+), 1 deletion(-) 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..66b3fe00 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -28,7 +28,7 @@ final class FileOperationsTest: XCTestCase { let writeBuf = UnsafeRawBufferPointer(rawBuf) let writeBufAddr = writeBuf.baseAddress - let syscallTestCases: Array = [ + var syscallTestCases: Array = [ MockTestCase(name: "open", .interruptable, "a path", O_RDWR | O_APPEND) { retryOnInterrupt in _ = try FileDescriptor.open( @@ -83,6 +83,27 @@ final class FileOperationsTest: XCTestCase { }, ] + #if !os(Windows) + syscallTestCases.append(contentsOf: [ + // 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 +224,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 } From ec315e99151346802f822b2723360b92ae23daa4 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Wed, 12 Oct 2022 11:01:54 -0600 Subject: [PATCH 02/26] wip: fcntl --- Sources/System/FileControl.swift | 217 +++++++ Sources/System/FileControlRaw.swift | 645 +++++++++++++++++++++ Tests/SystemTests/FileOperationsTest.swift | 20 + Tests/SystemTests/MockingTest.swift | 9 + Tests/SystemTests/XCTestManifests.swift | 21 +- 5 files changed, 911 insertions(+), 1 deletion(-) create mode 100644 Sources/System/FileControl.swift create mode 100644 Sources/System/FileControlRaw.swift diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift new file mode 100644 index 00000000..b53c7efb --- /dev/null +++ b/Sources/System/FileControl.swift @@ -0,0 +1,217 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +// Strongly typed, Swifty interfaces to the most common and useful `fcntl` +// commands. + +extension FileDescriptor { + /// Get the flags associated with this file descriptor + /// + /// The corresponding C function is `fcntl` with the `F_GETFD` command. + @_alwaysEmitIntoClient + public func getFlags() throws -> Flags { + try Flags(rawValue: fcntl(.getFlags)) + } + + /// Set the file descriptor flags. + /// + /// The corresponding C function is `fcntl` with the `F_SETFD` command. + @_alwaysEmitIntoClient + public func setFlags(_ value: Flags) throws { + _ = try fcntl(.setFlags, value.rawValue) + } + + /// Get descriptor status flags. + /// + /// The corresponding C function is `fcntl` with the `F_GETFL` command. + @_alwaysEmitIntoClient + public func getStatusFlags() throws -> StatusFlags { + try StatusFlags(rawValue: fcntl(.getStatusFlags)) + } + + /// Set descriptor status flags. + /// + /// The corresponding C function is `fcntl` with the `F_SETFL` command. + @_alwaysEmitIntoClient + public func setStatusFlags(_ flags: StatusFlags) throws { + _ = try fcntl(.setStatusFlags, flags.rawValue) + } +} + + +extension FileDescriptor { + /// Duplicate this file descriptor and return the newly created copy. + /// + /// - Parameters: + /// - `minRawValue`: A lower bound on the new file descriptor's raw value. + /// - `closeOnExec`: Whether the new descriptor's `closeOnExec` flag is set. + /// - Returns: The lowest numbered available descriptor whose raw value is + /// greater than or equal to `minRawValue`. + /// + /// File descriptors are merely references to some underlying system resource. + /// The system does not distinguish between the original and the new file + /// descriptor in any way. For example, read, write and seek operations on + /// one of them also affect the logical file position in the other, and + /// append mode, non-blocking I/O and asynchronous I/O options are shared + /// between the references. If a separate pointer into the file is desired, + /// a different object reference to the file must be obtained by issuing an + /// additional call to `open`. + /// + /// However, each file descriptor maintains its own close-on-exec flag. + /// + /// The corresponding C functions are `fcntl` with `F_DUPFD` and + /// `F_DUPFD_CLOEXEC`. + @_alwaysEmitIntoClient + public func duplicate( + minRawValue: CInt, closeOnExec: Bool + ) throws -> FileDescriptor { + let cmd: Command = closeOnExec ? .duplicateCloseOnExec : .duplicate + return try FileDescriptor(rawValue: fcntl(cmd, minRawValue)) + } + + #if !os(Linux) + /// Get the path of the file descriptor + /// + /// - Parameters: + /// - `noFirmLink`: Get the non firmlinked path of the file descriptor. + /// + /// The corresponding C functions are `fcntl` with `F_GETPATH` and + /// `F_GETPATH_NOFIRMLINK`. + public func getPath(noFirmLink: Bool = false) throws -> FilePath { + let cmd: Command = noFirmLink ? .getPathNoFirmLink : .getPath + // TODO: have a uninitialized init on FilePath / SystemString... + let bytes = try Array(unsafeUninitializedCapacity: _maxPathLen) { + (bufPtr, count: inout Int) in + _ = try fcntl(cmd, UnsafeMutableRawPointer(bufPtr.baseAddress!)) + count = 1 + system_strlen( + UnsafeRawPointer(bufPtr.baseAddress!).assumingMemoryBound(to: Int8.self)) + } + return FilePath(SystemString(nullTerminated: bytes)) + } + #endif +} + +// TODO: More fsync functionality using `F_BARRIERFSYNC` and `F_FULLFSYNC`, +// coinciding with the sketch for fsync. + +// MARK: - To add in the future with process support + +extension FileDescriptor { + // TODO: Flesh out PID work and see if there's a better, common formulation + // of the process-or-group id concept. + // TODO: @frozen + // TODO: public + private struct PIDOrPGID: RawRepresentable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + /// TODO: PID type + @_alwaysEmitIntoClient + public var asPID: CInt? { rawValue >= 0 ? rawValue : nil } + + /// TODO: PGID type + @_alwaysEmitIntoClient + public var asPositiveGroupID: CInt? { + rawValue >= 0 ? nil : -rawValue + } + + /// TODO: PID type + @_alwaysEmitIntoClient + public init(pid id: CInt) { + precondition(id >= 0) + self.init(rawValue: id) + } + + /// TODO: PGID type + @_alwaysEmitIntoClient + public init(positiveGroupID id: CInt) { + precondition(id >= 0) + self.init(rawValue: -id) + } + } + + /// Get the process ID or process group currently receiv- + /// ing SIGIO and SIGURG signals. + /// + /// The corresponding C function is `fcntl` with the `F_GETOWN` command. + // TODO: @_alwaysEmitIntoClient + // TODO: public + private func getOwner() throws -> PIDOrPGID { + try PIDOrPGID(rawValue: fcntl(.getOwner)) + } + + /// Set the process or process group to receive SIGIO and + /// SIGURG signals. + /// + /// The corresponding C function is `fcntl` with the `F_SETOWN` command. + // TODO: @_alwaysEmitIntoClient + // TODO: public + private func setOwner(_ id: PIDOrPGID) throws { + _ = try fcntl(.setOwner, id.rawValue) + } +} + +// MARK: - Removed and should be replaced + +// TODO: We don't want to highlight fcntl's highly inadvisable +// ("completely stupid" to quote the man page) locks. Instead, we'd want to provide +// `flock` on Darwin and some Linux equivalent. + +#if false + +// Record locking +extension FileDescriptor { + /// Get record locking information. + /// + /// Get the first lock that blocks the lock description described by `lock`. + /// If no lock is found that would prevent this lock from being created, the + /// structure is left unchanged by this function call except for the lock type + /// which is set to F_UNLCK. + /// + /// The corresponding C function is `fcntl` with `F_GETLK`. + @_alwaysEmitIntoClient + public func getLock(blocking lock: FileLock) throws -> FileLock { + var copy = lock + try _fcntlLock(.getLock, ©, retryOnInterrupt: false).get() + return copy + } + + /// Set record locking information. + /// + /// Set or clear a file segment lock according to the lock description + /// `lock`.`setLock` is used to establish shared/read locks (`FileLock.read`) + /// or exclusive/write locks, (`FileLock.write`), as well as remove either + /// type of lock (`FileLock.unlock`). If a shared or exclusive lock cannot be + /// set, this throws `Errno.resourceTemporarilyUnavailable`. + /// + /// If `waitIfBlocked` is set and a shared or exclusive lock is blocked by + /// other locks, the process waits until the request can be satisfied. If a + /// signal that is to be caught is received while this calls is waiting for a + /// region, the it will be interrupted if the signal handler has not + /// specified the SA_RESTART (see sigaction(2)). + /// + /// The corresponding C function is `fcntl` with `F_SETLK`. + @_alwaysEmitIntoClient + public func setLock( + to lock: FileLock, + waitIfBlocked: Bool = false, + retryOnInterrupt: Bool = true + ) throws { + let cmd: Command = waitIfBlocked ? .setLockWait : .setLock + var copy = lock + try _fcntlLock(cmd, ©, retryOnInterrupt: retryOnInterrupt).get() + // TODO: Does `fcntl` update `copy`? Should we return it? + } +} + +#endif + diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift new file mode 100644 index 00000000..df0048ea --- /dev/null +++ b/Sources/System/FileControlRaw.swift @@ -0,0 +1,645 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + + +#if !os(Windows) + +// RawRepresentable wrappers +extension FileDescriptor { + /// File descriptor flags. + @frozen + public struct Flags: OptionSet { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + /// The given file descriptor will be automatically closed in the + /// successor process image when one of the execv(2) or posix_spawn(2) + /// family of system calls is invoked. + /// + /// The corresponding C global is `FD_CLOEXEC`. + @_alwaysEmitIntoClient + public static var closeOnExec: Flags { Flags(rawValue: FD_CLOEXEC) } + } + + /// File descriptor status flags. + @frozen + public struct StatusFlags: OptionSet { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + fileprivate init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Non-blocking I/O; if no data is available to a read + /// call, or if a write operation would block, the read or + /// write call throws `Errno.resourceTemporarilyUnavailable`. + /// + /// The corresponding C constant is `O_NONBLOCK`. + @_alwaysEmitIntoClient + public static var nonBlocking: StatusFlags { StatusFlags(O_NONBLOCK) } + + /// Force each write to append at the end of file; corre- + /// sponds to `OpenOptions.append`. + /// + /// The corresponding C constant is `O_APPEND`. + @_alwaysEmitIntoClient + public static var append: StatusFlags { StatusFlags(O_APPEND) } + + /// Enable the SIGIO signal to be sent to the process + /// group when I/O is possible, e.g., upon availability of + /// data to be read. + /// + /// The corresponding C constant is `O_ASYNC`. + @_alwaysEmitIntoClient + public static var async: StatusFlags { StatusFlags(O_ASYNC) } + } + + +} + +// Raw escape hatch +extension FileDescriptor { + @usableFromInline + internal func _fcntl(_ cmd: Command) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue) + } + } + @usableFromInline + internal func _fcntl( + _ cmd: Command, _ arg: CInt + ) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue, arg) + } + } + @usableFromInline + internal func _fcntl( + _ cmd: Command, _ ptr: UnsafeMutableRawPointer + ) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue, ptr) + } + } + +//// @usableFromInline +// internal func _fcntlLock( +// _ cmd: Command, _ lock: inout FileLock, +// retryOnInterrupt: Bool +// ) -> Result<(), Errno> { +// nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { +// withUnsafeMutablePointer(to: &lock) { +// system_fcntl(self.rawValue, cmd.rawValue, $0) +// } +// } +// } + + /// Commands (and various constants) to pass to `fcntl`. + @frozen + public struct Command: RawRepresentable, Hashable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + private init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Duplicate file descriptor. + /// + /// Note: A Swiftier wrapper is + /// `FileDescriptor.duplicate(minRawValue:closeOnExec:)`. + /// + /// The corresponding C constant is `F_DUPFD`. + @_alwaysEmitIntoClient + public static var duplicate: Command { Command(F_DUPFD) } + + /// Get file descriptor flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getFlags()`. + /// + /// The corresponding C constant is `F_GETFD`. + @_alwaysEmitIntoClient + public static var getFlags: Command { Command(F_GETFD) } + + /// Set file descriptor flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setFlags(_:)`. + /// + /// The corresponding C constant is `F_SETFD`. + @_alwaysEmitIntoClient + public static var setFlags: Command { Command(F_SETFD) } + + /// Get file status flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getStatusFlags()`. + /// + /// The corresponding C constant is `F_GETFL`. + @_alwaysEmitIntoClient + public static var getStatusFlags: Command { + Command(F_GETFL) + } + + /// Set file status flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setStatusFlags(_:)`. + /// + /// The corresponding C constant is `F_SETFL`. + @_alwaysEmitIntoClient + public static var setStatusFlags: Command { + Command(F_SETFL) + } + + /// Get SIGIO/SIGURG proc/pgrp. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getOwner()`. + /// + /// The corresponding C constant is `F_GETOWN`. + @_alwaysEmitIntoClient + public static var getOwner: Command { Command(F_GETOWN) } + + /// Set SIGIO/SIGURG proc/pgrp. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setOwner(_:)`. + /// + /// The corresponding C constant is `F_SETOWN`. + @_alwaysEmitIntoClient + public static var setOwner: Command { Command(F_SETOWN) } + + /// Get record locking information. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getLock(_:_:)`. + /// + /// The corresponding C constant is `F_GETLK`. + @_alwaysEmitIntoClient + public static var getLock: Command { Command(F_GETLK) } + + /// Set record locking information. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. + /// + /// The corresponding C constant is `F_SETLK`. + @_alwaysEmitIntoClient + public static var setLock: Command { Command(F_SETLK) } + + /// Wait if blocked. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. + /// + /// The corresponding C constant is `F_SETLKW`. + @_alwaysEmitIntoClient + public static var setLockWait: Command { Command(F_SETLKW) } + + #if !os(Linux) + /// Wait if blocked, return on timeout. + /// + /// TODO: A Swiftier wrapper + /// + /// The corresponding C constant is `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public static var setLockWaitTimout: Command { + Command(F_SETLKWTIMEOUT) + } + + /// ??? TODO: Where is this documented? + /// + /// The corresponding C constant is `F_FLUSH_DATA`. + @_alwaysEmitIntoClient + public static var flushData: Command { + Command(F_FLUSH_DATA) + } + + /// Used for regression test. + /// + /// The corresponding C constant is `F_CHKCLEAN`. + @_alwaysEmitIntoClient + public static var checkClean: Command { Command(F_CHKCLEAN) } + + /// Preallocate storage. + /// + /// The corresponding C constant is `F_PREALLOCATE`. + @_alwaysEmitIntoClient + public static var preallocate: Command { + Command(F_PREALLOCATE) + } + + /// Truncate a file. Equivalent to calling truncate(2). + /// + /// The corresponding C constant is `F_SETSIZE`. + @_alwaysEmitIntoClient + public static var setSize: Command { Command(F_SETSIZE) } + + /// Issue an advisory read async with no copy to user. + /// + /// The corresponding C constant is `F_RDADVISE`. + @_alwaysEmitIntoClient + public static var readAdvise: Command { Command(F_RDADVISE) } + + /// Turn read ahead off/on for this fd. + /// + /// The corresponding C constant is `F_RDAHEAD`. + @_alwaysEmitIntoClient + public static var readAhead: Command { Command(F_RDAHEAD) } + + /// + /// Header says: 46,47 used to be F_READBOOTSTRAP and F_WRITEBOOTSTRAP + /// FIXME: What do we do here? + /// + + /// Turn data caching off/on for this fd. + /// + /// The corresponding C constant is `F_NOCACHE`. + @_alwaysEmitIntoClient + public static var noCache: Command { Command(F_NOCACHE) } + + /// File offset to device offset. + /// + /// The corresponding C constant is `F_LOG2PHYS`. + @_alwaysEmitIntoClient + public static var logicalToPhysical: Command { Command(F_LOG2PHYS) } + + /// Return the full path of the fd. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. + /// + /// The corresponding C constant is `F_GETPATH`. + @_alwaysEmitIntoClient + public static var getPath: Command { Command(F_GETPATH) } + + /// Fsync + ask the drive to flush to the media. + /// + /// The corresponding C constant is `F_FULLFSYNC`. + @_alwaysEmitIntoClient + public static var fullFsync: Command { Command(F_FULLFSYNC) } + + /// Find which component (if any) is a package. + /// + /// The corresponding C constant is `F_PATHPKG_CHECK`. + @_alwaysEmitIntoClient + public static var pathPackageCheck: Command { + Command(F_PATHPKG_CHECK) + } + + /// "Freeze" all fs operations. + /// + /// The corresponding C constant is `F_FREEZE_FS`. + @_alwaysEmitIntoClient + public static var freezeFileSystem: Command { Command(F_FREEZE_FS) } + + /// "Thaw" all fs operations. + /// + /// The corresponding C constant is `F_THAW_FS`. + @_alwaysEmitIntoClient + public static var thawFileSystem: Command { Command(F_THAW_FS) } + + /// Turn data caching off/on (globally) for this file. + /// + /// The corresponding C constant is `F_GLOBAL_NOCACHE`. + @_alwaysEmitIntoClient + public static var globalNoCache: Command { + Command(F_GLOBAL_NOCACHE) + } + + /// Add detached signatures. + /// + /// The corresponding C constant is `F_ADDSIGS`. + @_alwaysEmitIntoClient + public static var addSignatures: Command { + Command(F_ADDSIGS) + } + + /// Add signature from same file (used by dyld for shared libs). + /// + /// The corresponding C constant is `F_ADDFILESIGS`. + @_alwaysEmitIntoClient + public static var addFileSignatures: Command { + Command(F_ADDFILESIGS) + } + + /// Used in conjunction with F_NOCACHE to indicate that DIRECT, + /// synchonous writes should not be used (i.e. its ok to temporaily create + /// cached pages). + /// + /// The corresponding C constant is `F_NODIRECT`. + @_alwaysEmitIntoClient + public static var noDirect: Command { Command(F_NODIRECT) } + + /// Get the protection class of a file from the EA, returns int. + /// + /// The corresponding C constant is `F_GETPROTECTIONCLASS`. + @_alwaysEmitIntoClient + public static var getProtectionClass: Command { + Command(F_GETPROTECTIONCLASS) + } + + /// Set the protection class of a file for the EA, requires int. + /// + /// The corresponding C constant is `F_SETPROTECTIONCLASS`. + @_alwaysEmitIntoClient + public static var setProtectionClass: Command { + Command(F_SETPROTECTIONCLASS) + } + + /// File offset to device offset, extended. + /// + /// The corresponding C constant is `F_LOG2PHYS_EXT`. + @_alwaysEmitIntoClient + public static var logToPhysicalExtended: Command { + Command(F_LOG2PHYS_EXT) + } + + /// Get record locking information, per-process. + /// + /// The corresponding C constant is `F_GETLKPID`. + @_alwaysEmitIntoClient + public static var getLockPID: Command { Command(F_GETLKPID) } + #endif + + /// Mark the dup with FD_CLOEXEC. + /// + /// The corresponding C constant is `F_DUPFD_CLOEXEC` + @_alwaysEmitIntoClient + public static var duplicateCloseOnExec: Command { + Command(F_DUPFD_CLOEXEC) + } + + #if !os(Linux) + /// Mark the file as being the backing store for another filesystem. + /// + /// The corresponding C constant is `F_SETBACKINGSTORE`. + @_alwaysEmitIntoClient + public static var setBackingStore: Command { + Command(F_SETBACKINGSTORE) + } + + /// Return the full path of the FD, but error in specific mtmd + /// circumstances. + /// + /// The corresponding C constant is `F_GETPATH_MTMINFO`. + @_alwaysEmitIntoClient + public static var getPathMTMDInfo: Command { + Command(F_GETPATH_MTMINFO) + } + + /// Returns the code directory, with associated hashes, to the caller. + /// + /// The corresponding C constant is `F_GETCODEDIR`. + @_alwaysEmitIntoClient + public static var getCodeDirectory: Command { + Command(F_GETCODEDIR) + } + + /// No SIGPIPE generated on EPIPE. + /// + /// The corresponding C constant is `F_SETNOSIGPIPE`. + @_alwaysEmitIntoClient + public static var setNoSigPipe: Command { + Command(F_SETNOSIGPIPE) + } + + /// Status of SIGPIPE for this fd. + /// + /// The corresponding C constant is `F_GETNOSIGPIPE`. + @_alwaysEmitIntoClient + public static var getNoSigPipe: Command { + Command(F_GETNOSIGPIPE) + } + + /// For some cases, we need to rewrap the key for AKS/MKB. + /// + /// The corresponding C constant is `F_TRANSCODEKEY`. + @_alwaysEmitIntoClient + public static var transcodeKey: Command { + Command(F_TRANSCODEKEY) + } + + /// File being written to a by single writer... if throttling enabled, + /// writes may be broken into smaller chunks with throttling in between. + /// + /// The corresponding C constant is `F_SINGLE_WRITER`. + @_alwaysEmitIntoClient + public static var singleWriter: Command { + Command(F_SINGLE_WRITER) + } + + + /// Get the protection version number for this filesystem. + /// + /// The corresponding C constant is `F_GETPROTECTIONLEVEL`. + @_alwaysEmitIntoClient + public static var getProtectionLevel: Command { + Command(F_GETPROTECTIONLEVEL) + } + + + /// Add detached code signatures (used by dyld for shared libs). + /// + /// The corresponding C constant is `F_FINDSIGS`. + @_alwaysEmitIntoClient + public static var findSignatures: Command { + Command(F_FINDSIGS) + } + + /// Add signature from same file, only if it is signed by Apple (used by + /// dyld for simulator). + /// + /// The corresponding C constant is `F_ADDFILESIGS_FOR_DYLD_SIM`. + @_alwaysEmitIntoClient + public static var addFileSignaturesForDYLDSim: Command { + Command(F_ADDFILESIGS_FOR_DYLD_SIM) + } + + /// Fsync + issue barrier to drive. + /// + /// The corresponding C constant is `F_BARRIERFSYNC`. + @_alwaysEmitIntoClient + public static var barrierFsync: Command { + Command(F_BARRIERFSYNC) + } + + /// Add signature from same file, return end offset in structure on success. + /// + /// The corresponding C constant is `F_ADDFILESIGS_RETURN`. + @_alwaysEmitIntoClient + public static var addFileSignaturesReturn: Command { + Command(F_ADDFILESIGS_RETURN) + } + + /// Check if Library Validation allows this Mach-O file to be mapped into + /// the calling process. + /// + /// The corresponding C constant is `F_CHECK_LV`. + @_alwaysEmitIntoClient + public static var checkLibraryValidation: Command { + Command(F_CHECK_LV) + } + + /// Deallocate a range of the file. + /// + /// The corresponding C constant is `F_PUNCHHOLE`. + @_alwaysEmitIntoClient + public static var punchhole: Command { Command(F_PUNCHHOLE) } + + /// Trim an active file. + /// + /// The corresponding C constant is `F_TRIM_ACTIVE_FILE`. + @_alwaysEmitIntoClient + public static var trimActiveFile: Command { + Command(F_TRIM_ACTIVE_FILE) + } + + /// Synchronous advisory read fcntl for regular and compressed file. + /// + /// The corresponding C constant is `F_SPECULATIVE_READ`. + @_alwaysEmitIntoClient + public static var speculativeRead: Command { + Command(F_SPECULATIVE_READ) + } + + /// Return the full path without firmlinks of the fd. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. + /// + /// The corresponding C constant is `F_GETPATH_NOFIRMLINK`. + @_alwaysEmitIntoClient + public static var getPathNoFirmLink: Command { + Command(F_GETPATH_NOFIRMLINK) + } + + /// Add signature from same file, return information. + /// + /// The corresponding C constant is `F_ADDFILESIGS_INFO`. + @_alwaysEmitIntoClient + public static var addFileSignatureInfo: Command { + Command(F_ADDFILESIGS_INFO) + } + + /// Add supplemental signature from same file with fd reference to original. + /// + /// The corresponding C constant is `F_ADDFILESUPPL`. + @_alwaysEmitIntoClient + public static var addFileSupplementalSignature: Command { + Command(F_ADDFILESUPPL) + } + + /// Look up code signature information attached to a file or slice. + /// + /// The corresponding C constant is `F_GETSIGSINFO`. + @_alwaysEmitIntoClient + public static var getSignatureInfo: Command { + Command(F_GETSIGSINFO) + } + #endif + + /// Shared or read lock. + /// + /// The corresponding C constant is `F_RDLCK`. + @_alwaysEmitIntoClient + public static var readLock: Command { Command(F_RDLCK) } + + /// Unlock. + /// + /// The corresponding C constant is `F_UNLCK`. + @_alwaysEmitIntoClient + public static var unlock: Command { Command(F_UNLCK) } + + /// Exclusive or write lock. + /// + /// The corresponding C constant is `F_WRLCK`. + @_alwaysEmitIntoClient + public static var writeLock: Command { Command(F_WRLCK) } + + #if !os(Linux) + /// Allocate contigious space. + /// + /// TODO: This is actually a flag for the PREALLOCATE struct... + /// + /// The corresponding C constant is `F_ALLOCATECONTIG`. + @_alwaysEmitIntoClient + public static var allocateContiguous: Command { + Command(F_ALLOCATECONTIG) + } + + /// Allocate all requested space or no space at all. + /// + /// TODO: This is actually a flag for the PREALLOCATE struct... + /// + /// The corresponding C constant is `F_ALLOCATEALL`. + @_alwaysEmitIntoClient + public static var allocateAll: Command { + Command(F_ALLOCATEALL) + } + + /// Make it past all of the SEEK pos modes so that. + /// + /// TODO: The above is a direct quote from the header, figure out what it + /// means + /// + /// The corresponding C constant is `F_PEOFPOSMODE`. + @_alwaysEmitIntoClient + public static var endOfFile: Command { + Command(F_PEOFPOSMODE) + } + + /// Specify volume starting postion. + /// + /// The corresponding C constant is `F_VOLPOSMODE`. + @_alwaysEmitIntoClient + public static var startOfVolume: Command { + Command(F_VOLPOSMODE) + } + #endif + } + + /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func fcntl(_ cmd: Command) throws -> CInt { + try _fcntl(cmd).get() + } + + /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func fcntl(_ cmd: Command, _ arg: CInt) throws -> CInt { + try _fcntl(cmd, arg).get() + } + + /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func fcntl( + _ cmd: Command, _ ptr: UnsafeMutableRawPointer + ) throws -> CInt { + try _fcntl(cmd, ptr).get() + } +} + +#if !os(Linux) +internal var _maxPathLen: Int { Int(MAXPATHLEN) } +#endif + +#endif diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 66b3fe00..844e74b4 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -225,6 +225,26 @@ final class FileOperationsTest: XCTestCase { } } + func testFcntl() throws { + let path = FilePath("/tmp/\(UUID().uuidString).txt") + + // On Darwin, `/tmp` is a symlink to `/private/tmp` + // TODO: Linux? + // TODO: Get or query symlink status when we add those APIs + let resolvedPath = FilePath("/private").pushing(path.removingRoot()) + + let fd = try FileDescriptor.open( + path, .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) + try fd.closeAfter { + + #if !os(Linux) + XCTAssertEqual(resolvedPath, try fd.getPath()) + #endif + + // TODO: more tests + } + } + func testFlock() throws { // TODO: We need multiple processes in order to test blocking behavior } diff --git a/Tests/SystemTests/MockingTest.swift b/Tests/SystemTests/MockingTest.swift index 8d701e22..10006fa6 100644 --- a/Tests/SystemTests/MockingTest.swift +++ b/Tests/SystemTests/MockingTest.swift @@ -47,4 +47,13 @@ final class MockingTest: XCTestCase { } XCTAssertFalse(mockingEnabled) } + + func testFCNTLMocking() { + MockingDriver.withMockingEnabled { driver in + XCTAssertTrue(mockingEnabled) + + // TODO: a handful of mock tests, especially those that have different number of parameters + } + + } } diff --git a/Tests/SystemTests/XCTestManifests.swift b/Tests/SystemTests/XCTestManifests.swift index de99bd81..a4f4205a 100644 --- a/Tests/SystemTests/XCTestManifests.swift +++ b/Tests/SystemTests/XCTestManifests.swift @@ -1,4 +1,4 @@ -#if !canImport(ObjectiveC) && swift(<5.5) +#if !canImport(ObjectiveC) import XCTest extension ErrnoTest { @@ -28,8 +28,11 @@ extension FileOperationsTest { static let __allTests__FileOperationsTest = [ ("testAdHocOpen", testAdHocOpen), ("testAdHocPipe", testAdHocPipe), + ("testFcntl", testFcntl), + ("testFlock", testFlock), ("testGithubIssues", testGithubIssues), ("testHelpers", testHelpers), + ("testResizeFile", testResizeFile), ("testSyscalls", testSyscalls), ] } @@ -91,6 +94,7 @@ extension MockingTest { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__MockingTest = [ + ("testFCNTLMocking", testFCNTLMocking), ("testMocking", testMocking), ] } @@ -109,6 +113,21 @@ extension SystemStringTest { // `swift test --generate-linuxmain` // to regenerate. static let __allTests__SystemStringTest = [ + ("test_FilePath_initWithArrayConversion", test_FilePath_initWithArrayConversion), + ("test_FilePath_initWithInoutConversion", test_FilePath_initWithInoutConversion), + ("test_FilePath_initWithStringConversion", test_FilePath_initWithStringConversion), + ("test_FilePathComponent_initWithArrayConversion", test_FilePathComponent_initWithArrayConversion), + ("test_FilePathComponent_initWithInoutConversion", test_FilePathComponent_initWithInoutConversion), + ("test_FilePathComponent_initWithStringConversion", test_FilePathComponent_initWithStringConversion), + ("test_FilePathRoot_initWithArrayConversion", test_FilePathRoot_initWithArrayConversion), + ("test_FilePathRoot_initWithInoutConversion", test_FilePathRoot_initWithInoutConversion), + ("test_FilePathRoot_initWithStringConversion", test_FilePathRoot_initWithStringConversion), + ("test_String_initWithArrayConversion", test_String_initWithArrayConversion), + ("test_String_initWithInoutConversion", test_String_initWithInoutConversion), + ("test_String_initWithStringConversion", test_String_initWithStringConversion), + ("test_String_validatingPlatformStringWithArrayConversion", test_String_validatingPlatformStringWithArrayConversion), + ("test_String_validatingPlatformStringWithInoutConversion", test_String_validatingPlatformStringWithInoutConversion), + ("test_String_validatingPlatformStringWithStringConversion", test_String_validatingPlatformStringWithStringConversion), ("testAdHoc", testAdHoc), ("testPlatformString", testPlatformString), ] From d669abaad4c6f1649ad01ae56a63d20f6dd9c226 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Wed, 12 Oct 2022 18:32:51 -0600 Subject: [PATCH 03/26] wip --- Sources/System/FileControl.swift | 56 ----------------------------- Sources/System/FileControlRaw.swift | 29 ++------------- 2 files changed, 3 insertions(+), 82 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index b53c7efb..4e5032b1 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -159,59 +159,3 @@ extension FileDescriptor { _ = try fcntl(.setOwner, id.rawValue) } } - -// MARK: - Removed and should be replaced - -// TODO: We don't want to highlight fcntl's highly inadvisable -// ("completely stupid" to quote the man page) locks. Instead, we'd want to provide -// `flock` on Darwin and some Linux equivalent. - -#if false - -// Record locking -extension FileDescriptor { - /// Get record locking information. - /// - /// Get the first lock that blocks the lock description described by `lock`. - /// If no lock is found that would prevent this lock from being created, the - /// structure is left unchanged by this function call except for the lock type - /// which is set to F_UNLCK. - /// - /// The corresponding C function is `fcntl` with `F_GETLK`. - @_alwaysEmitIntoClient - public func getLock(blocking lock: FileLock) throws -> FileLock { - var copy = lock - try _fcntlLock(.getLock, ©, retryOnInterrupt: false).get() - return copy - } - - /// Set record locking information. - /// - /// Set or clear a file segment lock according to the lock description - /// `lock`.`setLock` is used to establish shared/read locks (`FileLock.read`) - /// or exclusive/write locks, (`FileLock.write`), as well as remove either - /// type of lock (`FileLock.unlock`). If a shared or exclusive lock cannot be - /// set, this throws `Errno.resourceTemporarilyUnavailable`. - /// - /// If `waitIfBlocked` is set and a shared or exclusive lock is blocked by - /// other locks, the process waits until the request can be satisfied. If a - /// signal that is to be caught is received while this calls is waiting for a - /// region, the it will be interrupted if the signal handler has not - /// specified the SA_RESTART (see sigaction(2)). - /// - /// The corresponding C function is `fcntl` with `F_SETLK`. - @_alwaysEmitIntoClient - public func setLock( - to lock: FileLock, - waitIfBlocked: Bool = false, - retryOnInterrupt: Bool = true - ) throws { - let cmd: Command = waitIfBlocked ? .setLockWait : .setLock - var copy = lock - try _fcntlLock(cmd, ©, retryOnInterrupt: retryOnInterrupt).get() - // TODO: Does `fcntl` update `copy`? Should we return it? - } -} - -#endif - diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index df0048ea..ddf03a81 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -75,8 +75,6 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var async: StatusFlags { StatusFlags(O_ASYNC) } } - - } // Raw escape hatch @@ -104,18 +102,6 @@ extension FileDescriptor { } } -//// @usableFromInline -// internal func _fcntlLock( -// _ cmd: Command, _ lock: inout FileLock, -// retryOnInterrupt: Bool -// ) -> Result<(), Errno> { -// nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { -// withUnsafeMutablePointer(to: &lock) { -// system_fcntl(self.rawValue, cmd.rawValue, $0) -// } -// } -// } - /// Commands (and various constants) to pass to `fcntl`. @frozen public struct Command: RawRepresentable, Hashable { @@ -185,30 +171,26 @@ extension FileDescriptor { /// /// Note: A Swiftier wrapper is `FileDescriptor.setOwner(_:)`. /// + /// TODO: `setOwner` is pending the `PIDOrPGID` type decision + /// /// The corresponding C constant is `F_SETOWN`. @_alwaysEmitIntoClient public static var setOwner: Command { Command(F_SETOWN) } /// Get record locking information. /// - /// Note: A Swiftier wrapper is `FileDescriptor.getLock(_:_:)`. - /// /// The corresponding C constant is `F_GETLK`. @_alwaysEmitIntoClient public static var getLock: Command { Command(F_GETLK) } /// Set record locking information. /// - /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. - /// /// The corresponding C constant is `F_SETLK`. @_alwaysEmitIntoClient public static var setLock: Command { Command(F_SETLK) } /// Wait if blocked. /// - /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. - /// /// The corresponding C constant is `F_SETLKW`. @_alwaysEmitIntoClient public static var setLockWait: Command { Command(F_SETLKW) } @@ -216,8 +198,6 @@ extension FileDescriptor { #if !os(Linux) /// Wait if blocked, return on timeout. /// - /// TODO: A Swiftier wrapper - /// /// The corresponding C constant is `F_SETLKWTIMEOUT`. @_alwaysEmitIntoClient public static var setLockWaitTimout: Command { @@ -593,10 +573,7 @@ extension FileDescriptor { Command(F_ALLOCATEALL) } - /// Make it past all of the SEEK pos modes so that. - /// - /// TODO: The above is a direct quote from the header, figure out what it - /// means + /// Allocate from the physical end of file. /// /// The corresponding C constant is `F_PEOFPOSMODE`. @_alwaysEmitIntoClient From 65bd8f8c7e0cc565769c803b1b20d5856513534a Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Mon, 24 Oct 2022 10:28:03 -0600 Subject: [PATCH 04/26] wip: ofd locks --- Sources/System/FileControlRaw.swift | 74 +++++++++++++++++++++--- Sources/System/Internals/Constants.swift | 49 ++++++++++++++++ 2 files changed, 115 insertions(+), 8 deletions(-) diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index ddf03a81..607e1f67 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -177,30 +177,88 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var setOwner: Command { Command(F_SETOWN) } - /// Get record locking information. + /// Get Open File Description record locking information. + /// + /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html + /// TODO: reference FileDesciptor.isLocked() or something like that /// /// The corresponding C constant is `F_GETLK`. @_alwaysEmitIntoClient - public static var getLock: Command { Command(F_GETLK) } + public static var getOFDLock: Command { Command(_F_OFD_GETLK) } - /// Set record locking information. + /// Set Open File Description record locking information. + /// + /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html + /// TODO: reference FileDesciptor.lock() /// /// The corresponding C constant is `F_SETLK`. @_alwaysEmitIntoClient - public static var setLock: Command { Command(F_SETLK) } + public static var setOFDLock: Command { Command(_F_OFD_SETLK) } - /// Wait if blocked. + /// Set Open File Description record locking information and wait until + /// the request can be completed. + /// + /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html + /// TODO: reference FileDesciptor.lock() /// /// The corresponding C constant is `F_SETLKW`. @_alwaysEmitIntoClient - public static var setLockWait: Command { Command(F_SETLKW) } + public static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } + + /// Set Open File Description record locking information and wait until + /// the request can be completed, returning on timeout. + /// + /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html + /// TODO: reference FileDesciptor.lock() + /// + /// The corresponding C constant is `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public static var setOFDLockWaitTimout: Command { + Command(_F_OFD_SETLKWTIMEOUT) + } + + /// Get POSIX process-level record locking information. + /// + /// Note: This implements POSIX.1 record locking semantics. The vast + /// majority of uses are better served by either OFD locks + /// (i.e. per-`open` locks) or `flock`-style per-process locks. + /// + /// The corresponding C constant is `F_GETLK`. + @_alwaysEmitIntoClient + public static var getPOSIXLock: Command { Command(F_GETLK) } + + /// Set POSIX process-level record locking information. + /// + /// Note: This implements POSIX.1 record locking semantics. The vast + /// majority of uses are better served by either OFD locks + /// (i.e. per-`open` locks) or `flock`-style per-process locks. + /// + /// The corresponding C constant is `F_SETLK`. + @_alwaysEmitIntoClient + public static var setPOSIXLock: Command { Command(F_SETLK) } + + /// Set POSIX process-level record locking information and wait until the + /// request can be completed. + /// + /// Note: This implements POSIX.1 record locking semantics. The vast + /// majority of uses are better served by either OFD locks + /// (i.e. per-`open` locks) or `flock`-style per-process locks. + /// + /// The corresponding C constant is `F_SETLKW`. + @_alwaysEmitIntoClient + public static var setPOSIXLockWait: Command { Command(F_SETLKW) } #if !os(Linux) - /// Wait if blocked, return on timeout. + /// Set POSIX process-level record locking information and wait until the + /// request can be completed, returning on timeout. + /// + /// Note: This implements POSIX.1 record locking semantics. The vast + /// majority of uses are better served by either OFD locks + /// (i.e. per-`open` locks) or `flock`-style per-process locks. /// /// The corresponding C constant is `F_SETLKWTIMEOUT`. @_alwaysEmitIntoClient - public static var setLockWaitTimout: Command { + public static var setPOSIXLockWaitTimout: Command { Command(F_SETLKWTIMEOUT) } diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index ec7a6f02..337fa5b8 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -541,3 +541,52 @@ internal var _LOCK_NB: CInt { LOCK_NB } @_alwaysEmitIntoClient internal var _LOCK_UN: CInt { LOCK_UN } #endif + +#if !os(Windows) +@_alwaysEmitIntoClient +internal var _F_OFD_SETLK: CInt { +#if os(Linux) + F_OFD_SETLK +#else + 90 // FIXME: use API when available +#endif +} + +@_alwaysEmitIntoClient +internal var _F_OFD_SETLKW: CInt { +#if os(Linux) + F_OFD_SETLKW +#else + 91 // FIXME: use API when available +#endif +} + +@_alwaysEmitIntoClient +internal var _F_OFD_GETLK: CInt { +#if os(Linux) + F_OFD_GETLK +#else + 92 // FIXME: use API when available +#endif +} + +@_alwaysEmitIntoClient +internal var _F_OFD_SETLKWTIMEOUT: CInt { +#if os(Linux) + F_OFD_SETLKWTIMEOUT +#else + 93 // FIXME: use API when available +#endif +} + +@_alwaysEmitIntoClient +internal var _F_OFD_GETLKPID: CInt { +#if os(Linux) + F_OFD_GETLKPID +#else + 94 // FIXME: use API when available +#endif +} +#endif // !os(Windows) + + From 6e96a7a963f434bd850741f30f47552e81475de1 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Mon, 24 Oct 2022 10:42:26 -0600 Subject: [PATCH 05/26] wip: linux ofd --- Sources/System/FileControlRaw.swift | 2 ++ Sources/System/Internals/Constants.swift | 18 ++++++------------ 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 607e1f67..34e22b3d 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -205,6 +205,7 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } +#if !os(Linux) /// Set Open File Description record locking information and wait until /// the request can be completed, returning on timeout. /// @@ -216,6 +217,7 @@ extension FileDescriptor { public static var setOFDLockWaitTimout: Command { Command(_F_OFD_SETLKWTIMEOUT) } +#endif /// Get POSIX process-level record locking information. /// diff --git a/Sources/System/Internals/Constants.swift b/Sources/System/Internals/Constants.swift index 337fa5b8..8d2dfd2b 100644 --- a/Sources/System/Internals/Constants.swift +++ b/Sources/System/Internals/Constants.swift @@ -546,7 +546,7 @@ internal var _LOCK_UN: CInt { LOCK_UN } @_alwaysEmitIntoClient internal var _F_OFD_SETLK: CInt { #if os(Linux) - F_OFD_SETLK + 37 // FIXME: F_OFD_SETLK #else 90 // FIXME: use API when available #endif @@ -555,7 +555,7 @@ internal var _F_OFD_SETLK: CInt { @_alwaysEmitIntoClient internal var _F_OFD_SETLKW: CInt { #if os(Linux) - F_OFD_SETLKW + 38 // FIXME: F_OFD_SETLKW #else 91 // FIXME: use API when available #endif @@ -564,29 +564,23 @@ internal var _F_OFD_SETLKW: CInt { @_alwaysEmitIntoClient internal var _F_OFD_GETLK: CInt { #if os(Linux) - F_OFD_GETLK + 36// FIXME: F_OFD_GETLK #else 92 // FIXME: use API when available #endif } +#if !os(Linux) @_alwaysEmitIntoClient internal var _F_OFD_SETLKWTIMEOUT: CInt { -#if os(Linux) - F_OFD_SETLKWTIMEOUT -#else 93 // FIXME: use API when available -#endif } - @_alwaysEmitIntoClient internal var _F_OFD_GETLKPID: CInt { -#if os(Linux) - F_OFD_GETLKPID -#else 94 // FIXME: use API when available -#endif } +#endif // !os(Linux) + #endif // !os(Windows) From f66e6ec525d47f972c64ff1c5f4517b3aad6b0bc Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Mon, 24 Oct 2022 13:09:00 -0600 Subject: [PATCH 06/26] wip: builds --- Sources/System/FileControl.swift | 33 ++-- Sources/System/FileControlRaw.swift | 217 ++++++++++++++++----- Sources/System/FileLock.swift | 128 +++++++++--- Sources/System/Internals/CInterop.swift | 30 ++- Tests/SystemTests/FileOperationsTest.swift | 40 ++-- 5 files changed, 331 insertions(+), 117 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index 4e5032b1..aaea6cbc 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -11,41 +11,46 @@ // commands. extension FileDescriptor { + // TODO: flags struct or individual queries? or a namespace with individual queries? + // These types aren't really `Control`s... + /// Get the flags associated with this file descriptor /// /// The corresponding C function is `fcntl` with the `F_GETFD` command. @_alwaysEmitIntoClient - public func getFlags() throws -> Flags { - try Flags(rawValue: fcntl(.getFlags)) + public func getFlags() throws -> Control.Flags { + try Control.Flags(rawValue: control(.getFlags)) } /// Set the file descriptor flags. /// /// The corresponding C function is `fcntl` with the `F_SETFD` command. @_alwaysEmitIntoClient - public func setFlags(_ value: Flags) throws { - _ = try fcntl(.setFlags, value.rawValue) + public func setFlags(_ value: Control.Flags) throws { + _ = try control(.setFlags, value.rawValue) } /// Get descriptor status flags. /// /// The corresponding C function is `fcntl` with the `F_GETFL` command. @_alwaysEmitIntoClient - public func getStatusFlags() throws -> StatusFlags { - try StatusFlags(rawValue: fcntl(.getStatusFlags)) + public func getStatusFlags() throws -> Control.StatusFlags { + try Control.StatusFlags(rawValue: control(.getStatusFlags)) } /// Set descriptor status flags. /// /// The corresponding C function is `fcntl` with the `F_SETFL` command. @_alwaysEmitIntoClient - public func setStatusFlags(_ flags: StatusFlags) throws { - _ = try fcntl(.setStatusFlags, flags.rawValue) + public func setStatusFlags(_ flags: Control.StatusFlags) throws { + _ = try control(.setStatusFlags, flags.rawValue) } } extension FileDescriptor { + // TODO: Unify this dup with the other dups which have come in since... + /// Duplicate this file descriptor and return the newly created copy. /// /// - Parameters: @@ -71,8 +76,8 @@ extension FileDescriptor { public func duplicate( minRawValue: CInt, closeOnExec: Bool ) throws -> FileDescriptor { - let cmd: Command = closeOnExec ? .duplicateCloseOnExec : .duplicate - return try FileDescriptor(rawValue: fcntl(cmd, minRawValue)) + let cmd: Control.Command = closeOnExec ? .duplicateCloseOnExec : .duplicate + return try FileDescriptor(rawValue: control(cmd, minRawValue)) } #if !os(Linux) @@ -84,11 +89,11 @@ extension FileDescriptor { /// The corresponding C functions are `fcntl` with `F_GETPATH` and /// `F_GETPATH_NOFIRMLINK`. public func getPath(noFirmLink: Bool = false) throws -> FilePath { - let cmd: Command = noFirmLink ? .getPathNoFirmLink : .getPath + let cmd: Control.Command = noFirmLink ? .getPathNoFirmLink : .getPath // TODO: have a uninitialized init on FilePath / SystemString... let bytes = try Array(unsafeUninitializedCapacity: _maxPathLen) { (bufPtr, count: inout Int) in - _ = try fcntl(cmd, UnsafeMutableRawPointer(bufPtr.baseAddress!)) + _ = try control(cmd, UnsafeMutableRawPointer(bufPtr.baseAddress!)) count = 1 + system_strlen( UnsafeRawPointer(bufPtr.baseAddress!).assumingMemoryBound(to: Int8.self)) } @@ -146,7 +151,7 @@ extension FileDescriptor { // TODO: @_alwaysEmitIntoClient // TODO: public private func getOwner() throws -> PIDOrPGID { - try PIDOrPGID(rawValue: fcntl(.getOwner)) + try PIDOrPGID(rawValue: control(.getOwner)) } /// Set the process or process group to receive SIGIO and @@ -156,6 +161,6 @@ extension FileDescriptor { // TODO: @_alwaysEmitIntoClient // TODO: public private func setOwner(_ id: PIDOrPGID) throws { - _ = try fcntl(.setOwner, id.rawValue) + _ = try control(.setOwner, id.rawValue) } } diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 34e22b3d..bd11805f 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -20,9 +20,25 @@ import Glibc #if !os(Windows) -// RawRepresentable wrappers extension FileDescriptor { + /// A namespace for types, values, and direct `fcntl` interfaces. + /// + /// TODO: a better name? "Internals", "Raw", "FCNTL"? I feel like a + /// precedent would be useful for sysctl, ioctl, and other grab-bag + /// things. "junk drawer" can be an anti-pattern, but is better than + /// trashing the higher namespace. + public enum Control {} +} + +// - MARK: RawRepresentable wrappers + +// TODO: What higher-level API should we expose? Individual predicates or get +// flags that will return these structs? If returning these structs, should +// they be in this namespace? +extension FileDescriptor.Control { /// File descriptor flags. + /// + /// These flags are not shared across duplicated file descriptors. @frozen public struct Flags: OptionSet { @_alwaysEmitIntoClient @@ -40,7 +56,11 @@ extension FileDescriptor { public static var closeOnExec: Flags { Flags(rawValue: FD_CLOEXEC) } } - /// File descriptor status flags. + /// File status flags. + /// + /// File status flags are associated with an open file description + /// (see `FileDescriptor.open`). Duplicated file descriptors + /// (see `FileDescriptor.duplicate`) share file status flags. @frozen public struct StatusFlags: OptionSet { @_alwaysEmitIntoClient @@ -75,33 +95,100 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var async: StatusFlags { StatusFlags(O_ASYNC) } } -} -// Raw escape hatch -extension FileDescriptor { - @usableFromInline - internal func _fcntl(_ cmd: Command) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue) + /// Advisory record locks. + /// + /// The corresponding C type is `struct flock`. + @frozen + public struct FileLock: RawRepresentable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FileLock + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileLock) { self.rawValue = rawValue } + + /// The type of the lock. + @frozen + public struct Kind: RawRepresentable, Hashable { + @_alwaysEmitIntoClient + public var rawValue: Int16 // TODO: Linux `short` too? `CShort`? + + @_alwaysEmitIntoClient + public init(rawValue: Int16) { self.rawValue = rawValue } + + /// Shared or read lock. + /// + /// The corresponding C constant is `F_RDLCK`. + @_alwaysEmitIntoClient + public static var readLock: Self { Self(rawValue: Int16(F_RDLCK)) } + + /// Unlock. + /// + /// The corresponding C constant is `F_UNLCK`. + @_alwaysEmitIntoClient + public static var unlock: Self { Self(rawValue: Int16(F_UNLCK)) } + + /// Exclusive or write lock. + /// + /// The corresponding C constant is `F_WRLCK`. + @_alwaysEmitIntoClient + public static var writeLock: Self { Self(rawValue: Int16(F_WRLCK)) } } - } - @usableFromInline - internal func _fcntl( - _ cmd: Command, _ arg: CInt - ) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue, arg) + + // TOOO: convenience initializers / static constructors + + /// The type of the locking operation. + /// + /// The corresponding C field is `l_type`. + @_alwaysEmitIntoClient + public var type: Kind { + get { Kind(rawValue: rawValue.l_type) } + set { rawValue.l_type = newValue.rawValue } } - } - @usableFromInline - internal func _fcntl( - _ cmd: Command, _ ptr: UnsafeMutableRawPointer - ) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue, ptr) + + /// The origin of the locked region. + /// + /// The corresponding C field is `l_whence`. + @_alwaysEmitIntoClient + public var origin: FileDescriptor.SeekOrigin { + get { FileDescriptor.SeekOrigin(rawValue: CInt(rawValue.l_whence)) } + set { rawValue.l_whence = Int16(newValue.rawValue) } + } + + /// The start offset (from the origin) of the locked region. + /// + /// The corresponding C field is `l_start`. + @_alwaysEmitIntoClient + public var start: Int64 { + get { Int64(rawValue.l_start) } + set { rawValue.l_start = CInterop.Offset(newValue) } + } + + /// The number of consecutive bytes to lock. + /// + /// The corresponding C field is `l_len`. + @_alwaysEmitIntoClient + public var length: Int64 { + get { Int64(rawValue.l_len) } + set { rawValue.l_len = CInterop.Offset(newValue) } } - } + /// The process ID of the lock holder, filled in by`FileDescriptor.getLock()`. + /// + /// TODO: Actual ProcessID type + /// + /// The corresponding C field is `l_pid` + @_alwaysEmitIntoClient + public var pid: CInterop.PID { + get { rawValue.l_pid } + set { rawValue.l_pid = newValue } + } + } +} + +// - MARK: Commands + +extension FileDescriptor.Control { /// Commands (and various constants) to pass to `fcntl`. @frozen public struct Command: RawRepresentable, Hashable { @@ -177,7 +264,7 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var setOwner: Command { Command(F_SETOWN) } - /// Get Open File Description record locking information. + /// Get open file description record locking information. /// /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html /// TODO: reference FileDesciptor.isLocked() or something like that @@ -186,7 +273,7 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var getOFDLock: Command { Command(_F_OFD_GETLK) } - /// Set Open File Description record locking information. + /// Set open file description record locking information. /// /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html /// TODO: reference FileDesciptor.lock() @@ -195,7 +282,7 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var setOFDLock: Command { Command(_F_OFD_SETLK) } - /// Set Open File Description record locking information and wait until + /// Set open file description record locking information and wait until /// the request can be completed. /// /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html @@ -206,7 +293,7 @@ extension FileDescriptor { public static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } #if !os(Linux) - /// Set Open File Description record locking information and wait until + /// Set open file description record locking information and wait until /// the request can be completed, returning on timeout. /// /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html @@ -227,7 +314,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `F_GETLK`. @_alwaysEmitIntoClient - public static var getPOSIXLock: Command { Command(F_GETLK) } + public static var getPOSIXProcessLock: Command { Command(F_GETLK) } /// Set POSIX process-level record locking information. /// @@ -237,7 +324,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `F_SETLK`. @_alwaysEmitIntoClient - public static var setPOSIXLock: Command { Command(F_SETLK) } + public static var setPOSIXProcessLock: Command { Command(F_SETLK) } /// Set POSIX process-level record locking information and wait until the /// request can be completed. @@ -248,7 +335,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `F_SETLKW`. @_alwaysEmitIntoClient - public static var setPOSIXLockWait: Command { Command(F_SETLKW) } + public static var setPOSIXProcessLockWait: Command { Command(F_SETLKW) } #if !os(Linux) /// Set POSIX process-level record locking information and wait until the @@ -260,7 +347,7 @@ extension FileDescriptor { /// /// The corresponding C constant is `F_SETLKWTIMEOUT`. @_alwaysEmitIntoClient - public static var setPOSIXLockWaitTimout: Command { + public static var setPOSIXProcessLockWaitTimout: Command { Command(F_SETLKWTIMEOUT) } @@ -594,24 +681,6 @@ extension FileDescriptor { } #endif - /// Shared or read lock. - /// - /// The corresponding C constant is `F_RDLCK`. - @_alwaysEmitIntoClient - public static var readLock: Command { Command(F_RDLCK) } - - /// Unlock. - /// - /// The corresponding C constant is `F_UNLCK`. - @_alwaysEmitIntoClient - public static var unlock: Command { Command(F_UNLCK) } - - /// Exclusive or write lock. - /// - /// The corresponding C constant is `F_WRLCK`. - @_alwaysEmitIntoClient - public static var writeLock: Command { Command(F_WRLCK) } - #if !os(Linux) /// Allocate contigious space. /// @@ -650,29 +719,69 @@ extension FileDescriptor { } #endif } +} + +// - MARK: Raw escape hatch + +extension FileDescriptor { + // TODO: better docs /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier /// alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient - public func fcntl(_ cmd: Command) throws -> CInt { + public func control(_ cmd: Control.Command) throws -> CInt { try _fcntl(cmd).get() } /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier /// alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient - public func fcntl(_ cmd: Command, _ arg: CInt) throws -> CInt { + public func control(_ cmd: Control.Command, _ arg: CInt) throws -> CInt { try _fcntl(cmd, arg).get() } /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier /// alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient - public func fcntl( - _ cmd: Command, _ ptr: UnsafeMutableRawPointer + public func control( + _ cmd: Control.Command, _ ptr: UnsafeMutableRawPointer ) throws -> CInt { try _fcntl(cmd, ptr).get() } + + @usableFromInline + internal func _fcntl(_ cmd: Control.Command) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue) + } + } + @usableFromInline + internal func _fcntl( + _ cmd: Control.Command, _ arg: CInt + ) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue, arg) + } + } + @usableFromInline + internal func _fcntl( + _ cmd: Control.Command, _ ptr: UnsafeMutableRawPointer + ) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue, ptr) + } + } + @usableFromInline + internal func _fcntlLock( + _ cmd: Control.Command, _ lock: inout Control.FileLock, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + withUnsafeMutablePointer(to: &lock) { + system_fcntl(self.rawValue, cmd.rawValue, $0) + } + } + } } #if !os(Linux) diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 91297e55..09786a33 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -1,54 +1,135 @@ #if !os(Windows) extension FileDescriptor { - /// Apply an advisory lock to the file associated with this descriptor. + /// The kind of a lock: read (aka "shared") or write (aka "exclusive") + public enum LockKind { + /// Read-only or shared lock + case read + + /// Write or exclusive lock + case write + + fileprivate var flockValue: Int16 /* TODO: short? */ { + // TODO: cleanup + switch self { + case .read: return FileDescriptor.Control.FileLock.Kind.readLock.rawValue + case .write: return FileDescriptor.Control.FileLock.Kind.writeLock.rawValue + } + } + } + + /// Get information about an open file description lock. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. /// - /// 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 + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code 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. + /// TODO: Something about what happens on fork + /// + /// FIXME: Any caveats for Darwin? /// - /// 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). + /// The corresponding C function is `fcntl` with `F_OFD_GETLK`. + @_alwaysEmitIntoClient + public func getLock() throws -> FileDescriptor.LockKind { + try _getLock().get() + } + + @usableFromInline + internal func _getLock() -> Result { + // FIXME: Do we need to issue two calls? From GNU: + // + // If there is a lock already in place that would block the lock described + // by the lockp argument, information about that lock is written + // to *lockp. Existing locks are not reported if they are compatible with + // making a new lock as specified. Thus, you should specify a lock type + // of F_WRLCK if you want to find out about both read and write locks, or + // F_RDLCK if you want to find out about write locks only. + // + // There might be more than one lock affecting the region specified by the + // lockp argument, but fcntl only returns information about one of them. + // Which lock is returned in this situation is undefined. + fatalError("TODO: implement") + } + + /// Set an open file description 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. + /// If the open file description already has a lock, the old lock is + /// replaced. If the lock cannot be set because it is blocked by an + /// existing lock on a file, `Errno.resourceTemporarilyUnavailable` is + /// 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. + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. /// - /// The corresponding C function is `flock()` + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// TODO: Something about what happens on fork + /// + /// FIXME: Any caveats for Darwin? + /// + /// FIXME: The non-wait isn't documented to throw EINTR, but fcnl in general + /// might. Do we do retry on interrupt or not? + /// + /// TODO: describe non-blocking + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or + /// `F_OFD_SETLKW`. @_alwaysEmitIntoClient public func lock( - exclusive: Bool = false, + kind: FileDescriptor.LockKind = .read, nonBlocking: Bool = false, retryOnInterrupt: Bool = true ) throws { - try _lock(exclusive: exclusive, nonBlocking: nonBlocking, retryOnInterrupt: retryOnInterrupt).get() + try _lock(kind: kind, nonBlocking: nonBlocking, retryOnInterrupt: retryOnInterrupt).get() } - /// Unlocks an existing advisory lock on the file associated with this descriptor. + /// Remove an open file description lock. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. /// - /// The corresponding C function is `flock` passed `LOCK_UN` + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// TODO: Something about what happens on fork + /// + /// FIXME: Any caveats for Darwin? + /// + /// FIXME: The non-wait isn't documented to throw EINTR, but fcnl in general + /// might. Do we do retry on interrupt or not? + /// + /// TODO: Do we need a non-blocking argument? Does that even make sense? + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` (TODO: or + /// `F_OFD_SETLKW`?) and a lock type of `F_UNLCK`. @_alwaysEmitIntoClient public func unlock(retryOnInterrupt: Bool = true) throws { try _unlock(retryOnInterrupt: retryOnInterrupt).get() - } @usableFromInline internal func _lock( - exclusive: Bool, + kind: FileDescriptor.LockKind, nonBlocking: Bool, retryOnInterrupt: Bool ) -> Result<(), Errno> { + // TODO: OFD locks var operation: CInt - if exclusive { + if kind == .write { operation = _LOCK_EX } else { operation = _LOCK_SH @@ -65,6 +146,7 @@ extension FileDescriptor { internal func _unlock( retryOnInterrupt: Bool ) -> Result<(), Errno> { + // TODO: OFD locks return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { system_flock(self.rawValue, _LOCK_UN) } diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index cb84a590..fa336d1e 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -38,29 +38,47 @@ public enum CInterop { /// The C `char` type public typealias Char = CChar - #if os(Windows) +#if os(Windows) /// The platform's preferred character type. On Unix, this is an 8-bit C /// `char` (which may be signed or unsigned, depending on platform). On /// Windows, this is `UInt16` (a "wide" character). public typealias PlatformChar = UInt16 - #else +#else /// The platform's preferred character type. On Unix, this is an 8-bit C /// `char` (which may be signed or unsigned, depending on platform). On /// Windows, this is `UInt16` (a "wide" character). public typealias PlatformChar = CInterop.Char - #endif +#endif - #if os(Windows) +#if os(Windows) /// The platform's preferred Unicode encoding. On Unix this is UTF-8 and on /// Windows it is UTF-16. Native strings may contain invalid Unicode, /// which will be handled by either error-correction or failing, depending /// on API. public typealias PlatformUnicodeEncoding = UTF16 - #else +#else /// The platform's preferred Unicode encoding. On Unix this is UTF-8 and on /// Windows it is UTF-16. Native strings may contain invalid Unicode, /// which will be handled by either error-correction or failing, depending /// on API. public typealias PlatformUnicodeEncoding = UTF8 - #endif +#endif + +#if !os(Windows) + /// The C `struct flock` type + public typealias FileLock = flock +#endif + + /// The C `pid_t` type + public typealias PID = pid_t + + /// The C `off_t` type. + /// + /// Note System generally standardizes on `Int64` where `off_t` + /// might otherwise appear. This typealias allows conversion code to be + /// emitted into client. + public typealias Offset = off_t + } + + diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 844e74b4..bb7ac8f5 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -83,26 +83,26 @@ final class FileOperationsTest: XCTestCase { }, ] - #if !os(Windows) - syscallTestCases.append(contentsOf: [ - // 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 +// #if !os(Windows) +// syscallTestCases.append(contentsOf: [ +// // 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() } } From 0baa67d391d06966856cff2eaecb141879c3b54b Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Mon, 24 Oct 2022 13:42:11 -0600 Subject: [PATCH 07/26] wip --- Sources/System/FileControl.swift | 83 +++++++++++++++++++++++++---- Sources/System/FileControlRaw.swift | 74 ++----------------------- Sources/System/FileLock.swift | 18 ++++--- 3 files changed, 91 insertions(+), 84 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index aaea6cbc..a1c8ad25 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -11,22 +11,19 @@ // commands. extension FileDescriptor { - // TODO: flags struct or individual queries? or a namespace with individual queries? - // These types aren't really `Control`s... - /// Get the flags associated with this file descriptor /// /// The corresponding C function is `fcntl` with the `F_GETFD` command. @_alwaysEmitIntoClient - public func getFlags() throws -> Control.Flags { - try Control.Flags(rawValue: control(.getFlags)) + public func getFlags() throws -> Flags { + try Flags(rawValue: control(.getFlags)) } /// Set the file descriptor flags. /// /// The corresponding C function is `fcntl` with the `F_SETFD` command. @_alwaysEmitIntoClient - public func setFlags(_ value: Control.Flags) throws { + public func setFlags(_ value: Flags) throws { _ = try control(.setFlags, value.rawValue) } @@ -34,15 +31,15 @@ extension FileDescriptor { /// /// The corresponding C function is `fcntl` with the `F_GETFL` command. @_alwaysEmitIntoClient - public func getStatusFlags() throws -> Control.StatusFlags { - try Control.StatusFlags(rawValue: control(.getStatusFlags)) + public func getStatusFlags() throws -> StatusFlags { + try StatusFlags(rawValue: control(.getStatusFlags)) } /// Set descriptor status flags. /// /// The corresponding C function is `fcntl` with the `F_SETFL` command. @_alwaysEmitIntoClient - public func setStatusFlags(_ flags: Control.StatusFlags) throws { + public func setStatusFlags(_ flags: StatusFlags) throws { _ = try control(.setStatusFlags, flags.rawValue) } } @@ -164,3 +161,71 @@ extension FileDescriptor { _ = try control(.setOwner, id.rawValue) } } + +extension FileDescriptor { + /// Low-level file control. + /// + /// Note: most common operations have Swiftier alternatives, e.g. `FileDescriptor.getFlags()` + /// + /// The corresponding C function is `fcntl`. + @_alwaysEmitIntoClient + public func control(_ cmd: Control.Command) throws -> CInt { + try _fcntl(cmd).get() + } + + /// Low-level file control. + /// + /// Note: most common operations have Swiftier alternatives, e.g. `FileDescriptor.getFlags()` + /// + /// The corresponding C function is `fcntl`. + @_alwaysEmitIntoClient + public func control(_ cmd: Control.Command, _ arg: CInt) throws -> CInt { + try _fcntl(cmd, arg).get() + } + + /// Low-level file control. + /// + /// Note: most common operations have Swiftier alternatives, e.g. `FileDescriptor.getFlags()` + /// + /// The corresponding C function is `fcntl`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Control.Command, _ ptr: UnsafeMutableRawPointer + ) throws -> CInt { + try _fcntl(cmd, ptr).get() + } + + @usableFromInline + internal func _fcntl(_ cmd: Control.Command) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue) + } + } + @usableFromInline + internal func _fcntl( + _ cmd: Control.Command, _ arg: CInt + ) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue, arg) + } + } + @usableFromInline + internal func _fcntl( + _ cmd: Control.Command, _ ptr: UnsafeMutableRawPointer + ) -> Result { + valueOrErrno(retryOnInterrupt: false) { + system_fcntl(self.rawValue, cmd.rawValue, ptr) + } + } + @usableFromInline + internal func _fcntl( + _ cmd: Control.Command, _ lock: inout Control.FileLock, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + withUnsafeMutablePointer(to: &lock) { + system_fcntl(self.rawValue, cmd.rawValue, $0) + } + } + } +} diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index bd11805f..a4779ae0 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -21,21 +21,14 @@ import Glibc #if !os(Windows) extension FileDescriptor { - /// A namespace for types, values, and direct `fcntl` interfaces. + /// A namespace for types and values for `FileDescriptor.control()`, aka `fcntl`. /// /// TODO: a better name? "Internals", "Raw", "FCNTL"? I feel like a /// precedent would be useful for sysctl, ioctl, and other grab-bag /// things. "junk drawer" can be an anti-pattern, but is better than /// trashing the higher namespace. public enum Control {} -} - -// - MARK: RawRepresentable wrappers -// TODO: What higher-level API should we expose? Individual predicates or get -// flags that will return these structs? If returning these structs, should -// they be in this namespace? -extension FileDescriptor.Control { /// File descriptor flags. /// /// These flags are not shared across duplicated file descriptors. @@ -95,7 +88,11 @@ extension FileDescriptor.Control { @_alwaysEmitIntoClient public static var async: StatusFlags { StatusFlags(O_ASYNC) } } +} + +// - MARK: RawRepresentable wrappers +extension FileDescriptor.Control { /// Advisory record locks. /// /// The corresponding C type is `struct flock`. @@ -723,67 +720,6 @@ extension FileDescriptor.Control { // - MARK: Raw escape hatch -extension FileDescriptor { - // TODO: better docs - - /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. - @_alwaysEmitIntoClient - public func control(_ cmd: Control.Command) throws -> CInt { - try _fcntl(cmd).get() - } - - /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. - @_alwaysEmitIntoClient - public func control(_ cmd: Control.Command, _ arg: CInt) throws -> CInt { - try _fcntl(cmd, arg).get() - } - - /// Raw interface to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. - @_alwaysEmitIntoClient - public func control( - _ cmd: Control.Command, _ ptr: UnsafeMutableRawPointer - ) throws -> CInt { - try _fcntl(cmd, ptr).get() - } - - @usableFromInline - internal func _fcntl(_ cmd: Control.Command) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue) - } - } - @usableFromInline - internal func _fcntl( - _ cmd: Control.Command, _ arg: CInt - ) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue, arg) - } - } - @usableFromInline - internal func _fcntl( - _ cmd: Control.Command, _ ptr: UnsafeMutableRawPointer - ) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue, ptr) - } - } - @usableFromInline - internal func _fcntlLock( - _ cmd: Control.Command, _ lock: inout Control.FileLock, - retryOnInterrupt: Bool - ) -> Result<(), Errno> { - nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { - withUnsafeMutablePointer(to: &lock) { - system_fcntl(self.rawValue, cmd.rawValue, $0) - } - } - } -} - #if !os(Linux) internal var _maxPathLen: Int { Int(MAXPATHLEN) } #endif diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 09786a33..162514bd 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -8,13 +8,14 @@ extension FileDescriptor { /// Write or exclusive lock case write - fileprivate var flockValue: Int16 /* TODO: short? */ { - // TODO: cleanup + fileprivate var flockValue: FileDescriptor.Control.FileLock.Kind { switch self { - case .read: return FileDescriptor.Control.FileLock.Kind.readLock.rawValue - case .write: return FileDescriptor.Control.FileLock.Kind.writeLock.rawValue + case .read: return .readLock + case .write: return .writeLock } } + + // FIXME: this wont work, because getLock can return unlock / none / clear. We should probably just put the raw representable struct here... } /// Get information about an open file description lock. @@ -53,7 +54,12 @@ extension FileDescriptor { // There might be more than one lock affecting the region specified by the // lockp argument, but fcntl only returns information about one of them. // Which lock is returned in this situation is undefined. - fatalError("TODO: implement") + + var flock = FileDescriptor.Control.FileLock(rawValue: CInterop.FileLock()) + flock.type = LockKind.write.flockValue + return self._fcntl(.getOFDLock, &flock).map { _ in + fatalError() + } } /// Set an open file description lock. @@ -87,7 +93,7 @@ extension FileDescriptor { @_alwaysEmitIntoClient public func lock( kind: FileDescriptor.LockKind = .read, - nonBlocking: Bool = false, + nonBlocking: Bool = false, // FIXME: "wait"? Which default? retryOnInterrupt: Bool = true ) throws { try _lock(kind: kind, nonBlocking: nonBlocking, retryOnInterrupt: retryOnInterrupt).get() From 1fa72c3795a448a1c3575d9416f49ca35b9982c0 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 15 Nov 2022 08:01:05 -0700 Subject: [PATCH 08/26] wip: ofd locks working --- Sources/System/FileControl.swift | 12 +- Sources/System/FileControlRaw.swift | 93 ----- Sources/System/FileLock.swift | 380 ++++++++++++++++----- Sources/System/Internals/CInterop.swift | 3 + Sources/System/Util.swift | 58 ++++ Tests/SystemTests/FileLockTest.swift | 106 ++++++ Tests/SystemTests/FileOperationsTest.swift | 4 - Tests/SystemTests/InternalUnitTests.swift | 52 +++ 8 files changed, 530 insertions(+), 178 deletions(-) create mode 100644 Tests/SystemTests/FileLockTest.swift create mode 100644 Tests/SystemTests/InternalUnitTests.swift diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index a1c8ad25..f75ac324 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -219,13 +219,19 @@ extension FileDescriptor { } @usableFromInline internal func _fcntl( - _ cmd: Control.Command, _ lock: inout Control.FileLock, + _ cmd: Control.Command, _ lock: inout FileDescriptor.FileLock, retryOnInterrupt: Bool ) -> Result<(), Errno> { - nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + print( + " fcntl \(cmd.rawValue) with type \(lock.type.rawValue)", + terminator: "") + let result = nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { withUnsafeMutablePointer(to: &lock) { - system_fcntl(self.rawValue, cmd.rawValue, $0) + let r = system_fcntl(self.rawValue, cmd.rawValue, $0) + return r } } + print(" -> type \(lock.type.rawValue)") + return result } } diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index a4779ae0..5cce109a 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -90,99 +90,6 @@ extension FileDescriptor { } } -// - MARK: RawRepresentable wrappers - -extension FileDescriptor.Control { - /// Advisory record locks. - /// - /// The corresponding C type is `struct flock`. - @frozen - public struct FileLock: RawRepresentable { - @_alwaysEmitIntoClient - public var rawValue: CInterop.FileLock - - @_alwaysEmitIntoClient - public init(rawValue: CInterop.FileLock) { self.rawValue = rawValue } - - /// The type of the lock. - @frozen - public struct Kind: RawRepresentable, Hashable { - @_alwaysEmitIntoClient - public var rawValue: Int16 // TODO: Linux `short` too? `CShort`? - - @_alwaysEmitIntoClient - public init(rawValue: Int16) { self.rawValue = rawValue } - - /// Shared or read lock. - /// - /// The corresponding C constant is `F_RDLCK`. - @_alwaysEmitIntoClient - public static var readLock: Self { Self(rawValue: Int16(F_RDLCK)) } - - /// Unlock. - /// - /// The corresponding C constant is `F_UNLCK`. - @_alwaysEmitIntoClient - public static var unlock: Self { Self(rawValue: Int16(F_UNLCK)) } - - /// Exclusive or write lock. - /// - /// The corresponding C constant is `F_WRLCK`. - @_alwaysEmitIntoClient - public static var writeLock: Self { Self(rawValue: Int16(F_WRLCK)) } - } - - // TOOO: convenience initializers / static constructors - - /// The type of the locking operation. - /// - /// The corresponding C field is `l_type`. - @_alwaysEmitIntoClient - public var type: Kind { - get { Kind(rawValue: rawValue.l_type) } - set { rawValue.l_type = newValue.rawValue } - } - - /// The origin of the locked region. - /// - /// The corresponding C field is `l_whence`. - @_alwaysEmitIntoClient - public var origin: FileDescriptor.SeekOrigin { - get { FileDescriptor.SeekOrigin(rawValue: CInt(rawValue.l_whence)) } - set { rawValue.l_whence = Int16(newValue.rawValue) } - } - - /// The start offset (from the origin) of the locked region. - /// - /// The corresponding C field is `l_start`. - @_alwaysEmitIntoClient - public var start: Int64 { - get { Int64(rawValue.l_start) } - set { rawValue.l_start = CInterop.Offset(newValue) } - } - - /// The number of consecutive bytes to lock. - /// - /// The corresponding C field is `l_len`. - @_alwaysEmitIntoClient - public var length: Int64 { - get { Int64(rawValue.l_len) } - set { rawValue.l_len = CInterop.Offset(newValue) } - } - - /// The process ID of the lock holder, filled in by`FileDescriptor.getLock()`. - /// - /// TODO: Actual ProcessID type - /// - /// The corresponding C field is `l_pid` - @_alwaysEmitIntoClient - public var pid: CInterop.PID { - get { rawValue.l_pid } - set { rawValue.l_pid = newValue } - } - } -} - // - MARK: Commands extension FileDescriptor.Control { diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 162514bd..dc033551 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -1,24 +1,175 @@ +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + #if !os(Windows) extension FileDescriptor { - /// The kind of a lock: read (aka "shared") or write (aka "exclusive") - public enum LockKind { - /// Read-only or shared lock - case read - - /// Write or exclusive lock - case write - - fileprivate var flockValue: FileDescriptor.Control.FileLock.Kind { - switch self { - case .read: return .readLock - case .write: return .writeLock - } + /// Advisory record locks. + /// + /// The corresponding C type is `struct flock`. + @frozen + public struct FileLock: RawRepresentable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FileLock + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FileLock) { self.rawValue = rawValue } + } +} + +extension FileDescriptor.FileLock { + /// TODO: docs + @_alwaysEmitIntoClient + public init() { self.init(rawValue: .init()) } + + /// The type of the locking operation. + /// + /// The corresponding C field is `l_type`. + @_alwaysEmitIntoClient + public var type: Kind { + get { Kind(rawValue: rawValue.l_type) } + set { rawValue.l_type = newValue.rawValue } + } + + /// The origin of the locked region. + /// + /// The corresponding C field is `l_whence`. + @_alwaysEmitIntoClient + public var origin: FileDescriptor.SeekOrigin { + get { FileDescriptor.SeekOrigin(rawValue: CInt(rawValue.l_whence)) } + set { rawValue.l_whence = Int16(newValue.rawValue) } + } + + /// The start offset (from the origin) of the locked region. + /// + /// The corresponding C field is `l_start`. + @_alwaysEmitIntoClient + public var start: Int64 { + get { Int64(rawValue.l_start) } + set { rawValue.l_start = CInterop.Offset(newValue) } + } + + /// The number of consecutive bytes to lock. + /// + /// The corresponding C field is `l_len`. + @_alwaysEmitIntoClient + public var length: Int64 { + get { Int64(rawValue.l_len) } + set { rawValue.l_len = CInterop.Offset(newValue) } + } + + /// The process ID of the lock holder, filled in by`FileDescriptor.getLock()`. + /// + /// TODO: Actual ProcessID type + /// + /// The corresponding C field is `l_pid` + @_alwaysEmitIntoClient + public var pid: CInterop.PID { + get { rawValue.l_pid } + set { rawValue.l_pid = newValue } + } +} + +// MARK: - Convenience for `struct flock` +extension FileDescriptor.FileLock { + + // For whole-file OFD locks + internal init( + ofdType: Kind, + start: Int64, + length: Int64 + ) { + self.init() + self.type = ofdType + self.start = start + self.length = length + self.pid = 0 + } + + // TOOO: convenience initializers or static constructors + + +} + +extension FileDescriptor.FileLock { + /// The kind of a lock: read ("shared"), write ("exclusive"), or none + /// ("unlock"). + @frozen + public struct Kind: RawRepresentable, Hashable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.CShort + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.CShort) { self.rawValue = rawValue } + + /// Read lock (aka "shared") + /// + /// The corresponding C constant is `F_RDLCK`. + @_alwaysEmitIntoClient + public static var read: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_RDLCK)) + } + + /// Write lock (aka "exclusive") + /// + /// The corresponding C constant is `F_WRLCK`. + @_alwaysEmitIntoClient + public static var write: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_WRLCK)) } - // FIXME: this wont work, because getLock can return unlock / none / clear. We should probably just put the raw representable struct here... + /// No lock (aka "unlock"). + /// + /// The corresponding C constant is `F_UNLCK`. + @_alwaysEmitIntoClient + public static var none: Self { + Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_UNLCK)) + } + } +} + + +// TODO: Need to version this carefully +// TODO: Don't do this, make a new type, but figure out ranges for that new type +extension FileDescriptor.SeekOrigin: Comparable, Strideable { + // TODO: Should stride be CInt or Int? + + public func distance(to other: FileDescriptor.SeekOrigin) -> Int { + Int(other.rawValue - self.rawValue) } - /// Get information about an open file description lock. + public func advanced(by n: Int) -> FileDescriptor.SeekOrigin { + .init(rawValue: self.rawValue + CInt(n)) + } + public static func < ( + lhs: FileDescriptor.SeekOrigin, rhs: FileDescriptor.SeekOrigin + ) -> Bool { + lhs.rawValue < rhs.rawValue + } +} + +extension FileDescriptor { + struct FileRange { + // Note: if it has an origin it wouldn't be comparable really or strideable + } +} + + +extension FileDescriptor { + /// All bytes in a file + /// + /// NOTE: We can't make byteRange optional _and_ generic in our API below because that requires type inference even when passed `nil`. + /// + @_alwaysEmitIntoClient + internal var _allFileBytes: Range { Int64.min ..< Int64.max } + + /// Get any conflicting locks held by other open file descriptions. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -30,44 +181,91 @@ extension FileDescriptor { /// (i.e. other code may still access files without using advisory locks /// possibly resulting in inconsistencies). /// - /// TODO: Something about what happens on fork + /// Open file description locks are inherited by child processes across + /// `fork`, etc. /// - /// FIXME: Any caveats for Darwin? + /// - Parameters: + /// - byteRange: The range of bytes over which to check for a lock. Pass + /// `nil` to consider the entire file (TODO: default value with Swift + /// 5.7) + /// - retryOnInterrupt: Whether to retry the open operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// - Returns; `.none` if there are no other locks, otherwise returns the + /// strongest conflicting lock /// /// The corresponding C function is `fcntl` with `F_OFD_GETLK`. + /// + /// FIXME: Does this only return OFD locks or process locks too? + /// TODO: document byte-range + /// FIXME: Can we just state the OFD docs once and link to it? + /// TODO: would a better API be e.g. `canGetLock(.read)`? or `getConflictingLock()`? + /// @_alwaysEmitIntoClient - public func getLock() throws -> FileDescriptor.LockKind { - try _getLock().get() + public func getConflictingLock( + byteRange: some RangeExpression, + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor.FileLock.Kind { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + return try _getConflictingLock( + start: start, length: len, retryOnInterrupt: retryOnInterrupt + ).get() + } + + + @_alwaysEmitIntoClient + public func getConflictingLock( + retryOnInterrupt: Bool = true + ) throws -> FileDescriptor.FileLock.Kind { + try getConflictingLock( + byteRange: _allFileBytes, retryOnInterrupt: retryOnInterrupt) } @usableFromInline - internal func _getLock() -> Result { - // FIXME: Do we need to issue two calls? From GNU: + internal func _getConflictingLock( + start: Int64, length: Int64, retryOnInterrupt: Bool + ) -> Result { + // If there are multiple locks already in place on a file region, the lock that + // is returned is unspecified. E.g. there could be a write lock over one + // portion of the file and a read lock over another overlapping + // region. Thus, we first check if there are any write locks, and if not + // we issue another call to check for any reads-or-writes. // - // If there is a lock already in place that would block the lock described - // by the lockp argument, information about that lock is written - // to *lockp. Existing locks are not reported if they are compatible with - // making a new lock as specified. Thus, you should specify a lock type - // of F_WRLCK if you want to find out about both read and write locks, or - // F_RDLCK if you want to find out about write locks only. - // - // There might be more than one lock affecting the region specified by the - // lockp argument, but fcntl only returns information about one of them. - // Which lock is returned in this situation is undefined. - - var flock = FileDescriptor.Control.FileLock(rawValue: CInterop.FileLock()) - flock.type = LockKind.write.flockValue - return self._fcntl(.getOFDLock, &flock).map { _ in - fatalError() + // 1) Try with a read lock, which will tell us if there's a conflicting + // write lock in place. + // + // 2) Try with a write lock, which will tell us if there's either a + // conflicting read or write lock in place. + var lock = FileDescriptor.FileLock(ofdType: .read, start: start, length: length) +// print(lock.type) + if case let .failure(err) = self._fcntl( + .getOFDLock, &lock, retryOnInterrupt: retryOnInterrupt + ) { + return .failure(err) + } + if lock.type == .write { +// print(lock.type) + return .success(.write) + } + guard lock.type == .none else { + fatalError("FIXME: really shouldn't be possible") } + // This means there was no conflicting lock, so try to detect reads + lock = FileDescriptor.FileLock(ofdType: .write, start: start, length: length) + + let secondTry = self._fcntl(.getOFDLock, &lock, retryOnInterrupt: retryOnInterrupt) +// print(lock.type) + return secondTry.map { lock.type } } /// Set an open file description lock. /// /// If the open file description already has a lock, the old lock is - /// replaced. If the lock cannot be set because it is blocked by an - /// existing lock on a file, `Errno.resourceTemporarilyUnavailable` is - /// thrown. + /// replaced. + /// + /// If the lock cannot be set because it is blocked by an existing lock on a + /// file and (TODO: blocking paremeter is false), + /// `Errno.resourceTemporarilyUnavailable` is thrown. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -79,12 +277,11 @@ extension FileDescriptor { /// (i.e. other code may still access files without using advisory locks /// possibly resulting in inconsistencies). /// - /// TODO: Something about what happens on fork - /// - /// FIXME: Any caveats for Darwin? + /// Open file description locks are inherited by child processes across + /// `fork`, etc. /// - /// FIXME: The non-wait isn't documented to throw EINTR, but fcnl in general - /// might. Do we do retry on interrupt or not? + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). /// /// TODO: describe non-blocking /// @@ -92,13 +289,35 @@ extension FileDescriptor { /// `F_OFD_SETLKW`. @_alwaysEmitIntoClient public func lock( - kind: FileDescriptor.LockKind = .read, - nonBlocking: Bool = false, // FIXME: "wait"? Which default? + _ kind: FileDescriptor.FileLock.Kind = .read, + byteRange: (some RangeExpression)? = nil, + nonBlocking: Bool = false, // FIXME: named "wait" or "blocking"? Which default is best? retryOnInterrupt: Bool = true ) throws { - try _lock(kind: kind, nonBlocking: nonBlocking, retryOnInterrupt: retryOnInterrupt).get() + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + try _lock( + kind, + start: start, + length: len, + nonBlocking: nonBlocking, + retryOnInterrupt: retryOnInterrupt + ).get() } + @_alwaysEmitIntoClient + public func lock( + _ kind: FileDescriptor.FileLock.Kind = .read, + nonBlocking: Bool = false, // FIXME: named "wait" or "blocking"? Which default is best? + retryOnInterrupt: Bool = true + ) throws { + try lock( + kind, + byteRange: _allFileBytes, + nonBlocking: nonBlocking, + retryOnInterrupt: retryOnInterrupt) + } + + /// Remove an open file description lock. /// /// Open file description locks are associated with an open file @@ -111,51 +330,56 @@ extension FileDescriptor { /// (i.e. other code may still access files without using advisory locks /// possibly resulting in inconsistencies). /// - /// TODO: Something about what happens on fork + /// Open file description locks are inherited by child processes across + /// `fork`, etc. /// - /// FIXME: Any caveats for Darwin? - /// - /// FIXME: The non-wait isn't documented to throw EINTR, but fcnl in general - /// might. Do we do retry on interrupt or not? + /// Calling `unlock()` is equivalent to passing `.none` as the lock kind to + /// `FileDescriptor.lock()`. /// /// TODO: Do we need a non-blocking argument? Does that even make sense? /// /// The corresponding C function is `fcntl` with `F_OFD_SETLK` (TODO: or /// `F_OFD_SETLKW`?) and a lock type of `F_UNLCK`. @_alwaysEmitIntoClient - public func unlock(retryOnInterrupt: Bool = true) throws { - try _unlock(retryOnInterrupt: retryOnInterrupt).get() + public func unlock( + byteRange: (some RangeExpression)? = nil, + nonBlocking: Bool = false, // FIXME: needed? + retryOnInterrupt: Bool = true + ) throws { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + try _lock( + .none, + start: start, + length: len, + nonBlocking: nonBlocking, + retryOnInterrupt: retryOnInterrupt + ).get() } - @usableFromInline - internal func _lock( - kind: FileDescriptor.LockKind, - nonBlocking: Bool, - retryOnInterrupt: Bool - ) -> Result<(), Errno> { - // TODO: OFD locks - var operation: CInt - if kind == .write { - operation = _LOCK_EX - } else { - operation = _LOCK_SH - } - if nonBlocking { - operation |= _LOCK_NB - } - return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_flock(self.rawValue, operation) - } + @_alwaysEmitIntoClient + public func unlock( + nonBlocking: Bool = false, // FIXME: needed? + retryOnInterrupt: Bool = true + ) throws { + try unlock( + byteRange: _allFileBytes, + nonBlocking: nonBlocking, + retryOnInterrupt: retryOnInterrupt) } + @usableFromInline - internal func _unlock( + internal func _lock( + _ kind: FileDescriptor.FileLock.Kind, + start: Int64, + length: Int64, + nonBlocking: Bool, retryOnInterrupt: Bool ) -> Result<(), Errno> { - // TODO: OFD locks - return nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_flock(self.rawValue, _LOCK_UN) - } + var lock = FileDescriptor.FileLock(ofdType: kind, start: start, length: length) + let command: FileDescriptor.Control.Command = + nonBlocking ? .setOFDLock : .setOFDLockWait + return _fcntl(command, &lock, retryOnInterrupt: retryOnInterrupt) } } #endif diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index fa336d1e..8f4b7119 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -38,6 +38,9 @@ public enum CInterop { /// The C `char` type public typealias Char = CChar + /// The C `short` type + public typealias CShort = Int16 + #if os(Windows) /// The platform's preferred character type. On Unix, this is an 8-bit C /// `char` (which may be signed or unsigned, depending on platform). On diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index ac25830f..a977eaab 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -127,3 +127,61 @@ extension MutableCollection where Element: Equatable { } } } + +/// Map byte offsets passed as a range expression to start+length, e.g. for +/// use in `struct flock`. +/// +/// Start can be negative, e.g. for use with `SEEK_CUR`. +/// +/// Convention: Half-open ranges or explicit `Int64.min` / `Int64.max` bounds +/// denote start or end. +/// +/// Passing `Int64.min` as the lower bound maps to a start offset of `0`, such +/// that `..<5` would map to `(start: 0, length: 5)`. +/// +/// Passing `Int64.max` as an upper bound maps to a length of `0` (i.e. rest +/// of file by convention), such that passing `5...` would map to `(start: 5, +/// length: 0)`. +/// +/// NOTE: This is a utility function and can return negative start offsets and +/// negative lengths E.g. `(-3)...` for user with `SEEK_CUR` and `...(-3)` +/// (TBD). It's up to the caller to check any additional invariants +/// +@_alwaysEmitIntoClient +internal func _mapByteRangeToByteOffsets( + _ byteRange: (some RangeExpression)? +) -> (start: Int64, length: Int64) { + let allInts = Int64.min.. + ) { + XCTAssertEqual(one, try ofd_1.getConflictingLock(byteRange: range)) + XCTAssertEqual(one, try dup_1.getConflictingLock(byteRange: range)) + + XCTAssertEqual(two, try ofd_2.getConflictingLock(byteRange: range)) + XCTAssertEqual(two, try dup_2.getConflictingLock(byteRange: range)) + } + + // TODO: tests + + } +} diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index bb7ac8f5..a9138dca 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -244,10 +244,6 @@ final class FileOperationsTest: XCTestCase { // TODO: more tests } } - - func testFlock() throws { - // TODO: We need multiple processes in order to test blocking behavior - } #endif } diff --git a/Tests/SystemTests/InternalUnitTests.swift b/Tests/SystemTests/InternalUnitTests.swift new file mode 100644 index 00000000..2a08b6ed --- /dev/null +++ b/Tests/SystemTests/InternalUnitTests.swift @@ -0,0 +1,52 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2020 - 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information +*/ + +import XCTest + +#if SYSTEM_PACKAGE +@testable import SystemPackage +#else +@testable import System +#endif + +final class InternalUnitTests: XCTestCase { + + + func testFileOffsets() { + func test( + _ br: (some RangeExpression)?, + _ expected: (start: Int64, length: Int64) + ) { + let (start, len) = _mapByteRangeToByteOffsets(br) + XCTAssertEqual(start, expected.start) + XCTAssertEqual(len, expected.length) + } + + test(2..<5, (start: 2, length: 3)) + test(2...5, (start: 2, length: 4)) + + test(..<5, (start: 0, length: 5)) + test(...5, (start: 0, length: 6)) + test(5..., (start: 5, length: 0)) + + // E.g. for use in specifying n bytes behind SEEK_CUR + // + // FIXME: are these ok? the API is for absolute + // offsets... + test((-3)..., (start: -3, length: 0)) + test((-3)..<0, (start: -3, length: 3)) + + // Non-sensical: up to the caller + test(..<(-5), (start: 0, length: -5)) + + } + + + +} From df8f08566f8ba771cfb0ab1beb385b1a3378d92b Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 15 Nov 2022 15:18:03 -0700 Subject: [PATCH 09/26] wip: cleanup --- Sources/System/FileLock.swift | 15 ++---- Sources/System/ProcessID.swift | 11 ++++ Tests/SystemTests/FileLockTest.swift | 80 +++++++++++++++------------- 3 files changed, 59 insertions(+), 47 deletions(-) create mode 100644 Sources/System/ProcessID.swift diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index dc033551..f1bbb3be 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -70,16 +70,15 @@ extension FileDescriptor.FileLock { /// /// The corresponding C field is `l_pid` @_alwaysEmitIntoClient - public var pid: CInterop.PID { - get { rawValue.l_pid } - set { rawValue.l_pid = newValue } + public var pid: ProcessID { + get { ProcessID(rawValue: rawValue.l_pid) } + set { rawValue.l_pid = newValue.rawValue } } } // MARK: - Convenience for `struct flock` extension FileDescriptor.FileLock { - - // For whole-file OFD locks + // For OFD locks internal init( ofdType: Kind, start: Int64, @@ -89,12 +88,8 @@ extension FileDescriptor.FileLock { self.type = ofdType self.start = start self.length = length - self.pid = 0 + self.pid = ProcessID(rawValue: 0) } - - // TOOO: convenience initializers or static constructors - - } extension FileDescriptor.FileLock { diff --git a/Sources/System/ProcessID.swift b/Sources/System/ProcessID.swift new file mode 100644 index 00000000..53442148 --- /dev/null +++ b/Sources/System/ProcessID.swift @@ -0,0 +1,11 @@ + +/// TODO: docs +@frozen +public struct ProcessID: RawRepresentable, Hashable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.PID + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.PID) { self.rawValue = rawValue } +} + diff --git a/Tests/SystemTests/FileLockTest.swift b/Tests/SystemTests/FileLockTest.swift index 9c7c4632..34a1c8ad 100644 --- a/Tests/SystemTests/FileLockTest.swift +++ b/Tests/SystemTests/FileLockTest.swift @@ -15,6 +15,10 @@ import XCTest @testable import System #endif +func _range(_ r: some RangeExpression) -> Range { + r.relative(to: Int64.min..? = nil + ) { + if let br = byteRange { + XCTAssertEqual(one, try ofd_1.getConflictingLock(byteRange: br)) + XCTAssertEqual(one, try dup_1.getConflictingLock(byteRange: br)) + + XCTAssertEqual(two, try ofd_2.getConflictingLock(byteRange: br)) + XCTAssertEqual(two, try dup_2.getConflictingLock(byteRange: br)) + } else { + XCTAssertEqual(one, try ofd_1.getConflictingLock()) + XCTAssertEqual(one, try dup_1.getConflictingLock()) + + XCTAssertEqual(two, try ofd_2.getConflictingLock()) + XCTAssertEqual(two, try dup_2.getConflictingLock()) + } } testOFDs(one: .none, two: .none) @@ -65,42 +81,32 @@ extension FileOperationsTest { } try ofd_1.unlock() - XCTAssertEqual(.read, try ofd_1.getConflictingLock()) - XCTAssertEqual(.read, try dup_1.getConflictingLock()) - XCTAssertEqual(.none, try ofd_2.getConflictingLock()) - XCTAssertEqual(.none, try dup_2.getConflictingLock()) - - try dup_2.lock(.write, nonBlocking: true) - XCTAssertEqual(.write, try ofd_1.getConflictingLock()) - XCTAssertEqual(.write, try dup_1.getConflictingLock()) - XCTAssertEqual(.none, try ofd_2.getConflictingLock()) - XCTAssertEqual(.none, try dup_2.getConflictingLock()) - } - - func testFileLockByteRanges() throws { - let path = FilePath("/tmp/\(UUID().uuidString).txt") + try ofd_2.unlock() + testOFDs(one: .none, two: .none) - let ofd_1 = try FileDescriptor.open( - path, .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) - let dup_1 = try ofd_1.duplicate() + /// Byte ranges - let ofd_2 = try FileDescriptor.open( - path, .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) - let dup_2 = try ofd_2.duplicate() + try dup_1.lock(byteRange: ..<50) + testOFDs(one: .none, two: .read) + testOFDs(one: .none, two: .none, byteRange: _range(51...)) + testOFDs(one: .none, two: .read, byteRange: _range(1..<2)) - func testOFDs( - one: FileDescriptor.FileLock.Kind, - two: FileDescriptor.FileLock.Kind, - byteRange range: Range - ) { - XCTAssertEqual(one, try ofd_1.getConflictingLock(byteRange: range)) - XCTAssertEqual(one, try dup_1.getConflictingLock(byteRange: range)) + try dup_1.lock(.write, byteRange: 100..<150) + testOFDs(one: .none, two: .write) + testOFDs(one: .none, two: .read, byteRange: 49..<50) + testOFDs(one: .none, two: .none, byteRange: 98..<99) + testOFDs(one: .none, two: .write, byteRange: _range(100...)) - XCTAssertEqual(two, try ofd_2.getConflictingLock(byteRange: range)) - XCTAssertEqual(two, try dup_2.getConflictingLock(byteRange: range)) - } + try dup_1.unlock(byteRange: ..<49) + testOFDs(one: .none, two: .read, byteRange: 49..<50) - // TODO: tests + try dup_1.unlock(byteRange: ..<149) + testOFDs(one: .none, two: .write) + testOFDs(one: .none, two: .none, byteRange: _range(..<149)) + testOFDs(one: .none, two: .write, byteRange: 149..<150) + try dup_1.unlock(byteRange: 149..<150) + testOFDs(one: .none, two: .none) } } + From 64040f38fc54291e2869c7863be8de6857d05649 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 15 Nov 2022 15:23:09 -0700 Subject: [PATCH 10/26] wip: removing fcntl API --- Sources/System/FileControl.swift | 218 +-------------------- Tests/SystemTests/FileOperationsTest.swift | 20 -- 2 files changed, 3 insertions(+), 235 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index f75ac324..eb9ffd93 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -5,233 +5,21 @@ Licensed under Apache License v2.0 with Runtime Library Exception See https://swift.org/LICENSE.txt for license information -*/ + */ // Strongly typed, Swifty interfaces to the most common and useful `fcntl` // commands. extension FileDescriptor { - /// Get the flags associated with this file descriptor - /// - /// The corresponding C function is `fcntl` with the `F_GETFD` command. - @_alwaysEmitIntoClient - public func getFlags() throws -> Flags { - try Flags(rawValue: control(.getFlags)) - } - - /// Set the file descriptor flags. - /// - /// The corresponding C function is `fcntl` with the `F_SETFD` command. - @_alwaysEmitIntoClient - public func setFlags(_ value: Flags) throws { - _ = try control(.setFlags, value.rawValue) - } - - /// Get descriptor status flags. - /// - /// The corresponding C function is `fcntl` with the `F_GETFL` command. - @_alwaysEmitIntoClient - public func getStatusFlags() throws -> StatusFlags { - try StatusFlags(rawValue: control(.getStatusFlags)) - } - - /// Set descriptor status flags. - /// - /// The corresponding C function is `fcntl` with the `F_SETFL` command. - @_alwaysEmitIntoClient - public func setStatusFlags(_ flags: StatusFlags) throws { - _ = try control(.setStatusFlags, flags.rawValue) - } -} - - -extension FileDescriptor { - // TODO: Unify this dup with the other dups which have come in since... - - /// Duplicate this file descriptor and return the newly created copy. - /// - /// - Parameters: - /// - `minRawValue`: A lower bound on the new file descriptor's raw value. - /// - `closeOnExec`: Whether the new descriptor's `closeOnExec` flag is set. - /// - Returns: The lowest numbered available descriptor whose raw value is - /// greater than or equal to `minRawValue`. - /// - /// File descriptors are merely references to some underlying system resource. - /// The system does not distinguish between the original and the new file - /// descriptor in any way. For example, read, write and seek operations on - /// one of them also affect the logical file position in the other, and - /// append mode, non-blocking I/O and asynchronous I/O options are shared - /// between the references. If a separate pointer into the file is desired, - /// a different object reference to the file must be obtained by issuing an - /// additional call to `open`. - /// - /// However, each file descriptor maintains its own close-on-exec flag. - /// - /// The corresponding C functions are `fcntl` with `F_DUPFD` and - /// `F_DUPFD_CLOEXEC`. - @_alwaysEmitIntoClient - public func duplicate( - minRawValue: CInt, closeOnExec: Bool - ) throws -> FileDescriptor { - let cmd: Control.Command = closeOnExec ? .duplicateCloseOnExec : .duplicate - return try FileDescriptor(rawValue: control(cmd, minRawValue)) - } - - #if !os(Linux) - /// Get the path of the file descriptor - /// - /// - Parameters: - /// - `noFirmLink`: Get the non firmlinked path of the file descriptor. - /// - /// The corresponding C functions are `fcntl` with `F_GETPATH` and - /// `F_GETPATH_NOFIRMLINK`. - public func getPath(noFirmLink: Bool = false) throws -> FilePath { - let cmd: Control.Command = noFirmLink ? .getPathNoFirmLink : .getPath - // TODO: have a uninitialized init on FilePath / SystemString... - let bytes = try Array(unsafeUninitializedCapacity: _maxPathLen) { - (bufPtr, count: inout Int) in - _ = try control(cmd, UnsafeMutableRawPointer(bufPtr.baseAddress!)) - count = 1 + system_strlen( - UnsafeRawPointer(bufPtr.baseAddress!).assumingMemoryBound(to: Int8.self)) - } - return FilePath(SystemString(nullTerminated: bytes)) - } - #endif -} - -// TODO: More fsync functionality using `F_BARRIERFSYNC` and `F_FULLFSYNC`, -// coinciding with the sketch for fsync. - -// MARK: - To add in the future with process support - -extension FileDescriptor { - // TODO: Flesh out PID work and see if there's a better, common formulation - // of the process-or-group id concept. - // TODO: @frozen - // TODO: public - private struct PIDOrPGID: RawRepresentable { - @_alwaysEmitIntoClient - public let rawValue: CInt - - @_alwaysEmitIntoClient - public init(rawValue: CInt) { self.rawValue = rawValue } - - /// TODO: PID type - @_alwaysEmitIntoClient - public var asPID: CInt? { rawValue >= 0 ? rawValue : nil } - - /// TODO: PGID type - @_alwaysEmitIntoClient - public var asPositiveGroupID: CInt? { - rawValue >= 0 ? nil : -rawValue - } - - /// TODO: PID type - @_alwaysEmitIntoClient - public init(pid id: CInt) { - precondition(id >= 0) - self.init(rawValue: id) - } - - /// TODO: PGID type - @_alwaysEmitIntoClient - public init(positiveGroupID id: CInt) { - precondition(id >= 0) - self.init(rawValue: -id) - } - } - - /// Get the process ID or process group currently receiv- - /// ing SIGIO and SIGURG signals. - /// - /// The corresponding C function is `fcntl` with the `F_GETOWN` command. - // TODO: @_alwaysEmitIntoClient - // TODO: public - private func getOwner() throws -> PIDOrPGID { - try PIDOrPGID(rawValue: control(.getOwner)) - } - - /// Set the process or process group to receive SIGIO and - /// SIGURG signals. - /// - /// The corresponding C function is `fcntl` with the `F_SETOWN` command. - // TODO: @_alwaysEmitIntoClient - // TODO: public - private func setOwner(_ id: PIDOrPGID) throws { - _ = try control(.setOwner, id.rawValue) - } -} - -extension FileDescriptor { - /// Low-level file control. - /// - /// Note: most common operations have Swiftier alternatives, e.g. `FileDescriptor.getFlags()` - /// - /// The corresponding C function is `fcntl`. - @_alwaysEmitIntoClient - public func control(_ cmd: Control.Command) throws -> CInt { - try _fcntl(cmd).get() - } - - /// Low-level file control. - /// - /// Note: most common operations have Swiftier alternatives, e.g. `FileDescriptor.getFlags()` - /// - /// The corresponding C function is `fcntl`. - @_alwaysEmitIntoClient - public func control(_ cmd: Control.Command, _ arg: CInt) throws -> CInt { - try _fcntl(cmd, arg).get() - } - - /// Low-level file control. - /// - /// Note: most common operations have Swiftier alternatives, e.g. `FileDescriptor.getFlags()` - /// - /// The corresponding C function is `fcntl`. - @_alwaysEmitIntoClient - public func control( - _ cmd: Control.Command, _ ptr: UnsafeMutableRawPointer - ) throws -> CInt { - try _fcntl(cmd, ptr).get() - } - - @usableFromInline - internal func _fcntl(_ cmd: Control.Command) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue) - } - } - @usableFromInline - internal func _fcntl( - _ cmd: Control.Command, _ arg: CInt - ) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue, arg) - } - } - @usableFromInline - internal func _fcntl( - _ cmd: Control.Command, _ ptr: UnsafeMutableRawPointer - ) -> Result { - valueOrErrno(retryOnInterrupt: false) { - system_fcntl(self.rawValue, cmd.rawValue, ptr) - } - } @usableFromInline internal func _fcntl( _ cmd: Control.Command, _ lock: inout FileDescriptor.FileLock, retryOnInterrupt: Bool ) -> Result<(), Errno> { - print( - " fcntl \(cmd.rawValue) with type \(lock.type.rawValue)", - terminator: "") - let result = nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { withUnsafeMutablePointer(to: &lock) { - let r = system_fcntl(self.rawValue, cmd.rawValue, $0) - return r + system_fcntl(self.rawValue, cmd.rawValue, $0) } } - print(" -> type \(lock.type.rawValue)") - return result } } diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index a9138dca..14908f08 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -224,26 +224,6 @@ final class FileOperationsTest: XCTestCase { XCTAssertEqual(readBytesAfterTruncation, Array("ab".utf8)) } } - - func testFcntl() throws { - let path = FilePath("/tmp/\(UUID().uuidString).txt") - - // On Darwin, `/tmp` is a symlink to `/private/tmp` - // TODO: Linux? - // TODO: Get or query symlink status when we add those APIs - let resolvedPath = FilePath("/private").pushing(path.removingRoot()) - - let fd = try FileDescriptor.open( - path, .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) - try fd.closeAfter { - - #if !os(Linux) - XCTAssertEqual(resolvedPath, try fd.getPath()) - #endif - - // TODO: more tests - } - } #endif } From b32fa0ed28eda8ab35d3314e5a7753ba011f299a Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 15 Nov 2022 15:31:47 -0700 Subject: [PATCH 11/26] wip: drop more fcntl API from this PR --- Sources/System/FileControl.swift | 1 - Sources/System/FileControlRaw.swift | 581 ++-------------------------- 2 files changed, 23 insertions(+), 559 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index eb9ffd93..6b5e050c 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -11,7 +11,6 @@ // commands. extension FileDescriptor { - @usableFromInline internal func _fcntl( _ cmd: Control.Command, _ lock: inout FileDescriptor.FileLock, retryOnInterrupt: Bool diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 5cce109a..61f134ba 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -27,155 +27,36 @@ extension FileDescriptor { /// precedent would be useful for sysctl, ioctl, and other grab-bag /// things. "junk drawer" can be an anti-pattern, but is better than /// trashing the higher namespace. - public enum Control {} + // public + internal enum Control {} - /// File descriptor flags. - /// - /// These flags are not shared across duplicated file descriptors. - @frozen - public struct Flags: OptionSet { - @_alwaysEmitIntoClient - public let rawValue: CInt - - @_alwaysEmitIntoClient - public init(rawValue: CInt) { self.rawValue = rawValue } - - /// The given file descriptor will be automatically closed in the - /// successor process image when one of the execv(2) or posix_spawn(2) - /// family of system calls is invoked. - /// - /// The corresponding C global is `FD_CLOEXEC`. - @_alwaysEmitIntoClient - public static var closeOnExec: Flags { Flags(rawValue: FD_CLOEXEC) } - } - - /// File status flags. - /// - /// File status flags are associated with an open file description - /// (see `FileDescriptor.open`). Duplicated file descriptors - /// (see `FileDescriptor.duplicate`) share file status flags. - @frozen - public struct StatusFlags: OptionSet { - @_alwaysEmitIntoClient - public let rawValue: CInt - - @_alwaysEmitIntoClient - public init(rawValue: CInt) { self.rawValue = rawValue } - - @_alwaysEmitIntoClient - fileprivate init(_ raw: CInt) { self.init(rawValue: raw) } - - /// Non-blocking I/O; if no data is available to a read - /// call, or if a write operation would block, the read or - /// write call throws `Errno.resourceTemporarilyUnavailable`. - /// - /// The corresponding C constant is `O_NONBLOCK`. - @_alwaysEmitIntoClient - public static var nonBlocking: StatusFlags { StatusFlags(O_NONBLOCK) } - - /// Force each write to append at the end of file; corre- - /// sponds to `OpenOptions.append`. - /// - /// The corresponding C constant is `O_APPEND`. - @_alwaysEmitIntoClient - public static var append: StatusFlags { StatusFlags(O_APPEND) } - - /// Enable the SIGIO signal to be sent to the process - /// group when I/O is possible, e.g., upon availability of - /// data to be read. - /// - /// The corresponding C constant is `O_ASYNC`. - @_alwaysEmitIntoClient - public static var async: StatusFlags { StatusFlags(O_ASYNC) } - } } - // - MARK: Commands extension FileDescriptor.Control { /// Commands (and various constants) to pass to `fcntl`. - @frozen - public struct Command: RawRepresentable, Hashable { - @_alwaysEmitIntoClient - public let rawValue: CInt + // @frozen + // public + internal struct Command: RawRepresentable, Hashable { +// @_alwaysEmitIntoClient +// public + internal let rawValue: CInt - @_alwaysEmitIntoClient - public init(rawValue: CInt) { self.rawValue = rawValue } +// @_alwaysEmitIntoClient +// public + internal init(rawValue: CInt) { self.rawValue = rawValue } @_alwaysEmitIntoClient private init(_ raw: CInt) { self.init(rawValue: raw) } - - /// Duplicate file descriptor. - /// - /// Note: A Swiftier wrapper is - /// `FileDescriptor.duplicate(minRawValue:closeOnExec:)`. - /// - /// The corresponding C constant is `F_DUPFD`. - @_alwaysEmitIntoClient - public static var duplicate: Command { Command(F_DUPFD) } - - /// Get file descriptor flags. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getFlags()`. - /// - /// The corresponding C constant is `F_GETFD`. - @_alwaysEmitIntoClient - public static var getFlags: Command { Command(F_GETFD) } - - /// Set file descriptor flags. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.setFlags(_:)`. - /// - /// The corresponding C constant is `F_SETFD`. - @_alwaysEmitIntoClient - public static var setFlags: Command { Command(F_SETFD) } - - /// Get file status flags. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getStatusFlags()`. - /// - /// The corresponding C constant is `F_GETFL`. - @_alwaysEmitIntoClient - public static var getStatusFlags: Command { - Command(F_GETFL) - } - - /// Set file status flags. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.setStatusFlags(_:)`. - /// - /// The corresponding C constant is `F_SETFL`. - @_alwaysEmitIntoClient - public static var setStatusFlags: Command { - Command(F_SETFL) - } - - /// Get SIGIO/SIGURG proc/pgrp. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getOwner()`. - /// - /// The corresponding C constant is `F_GETOWN`. - @_alwaysEmitIntoClient - public static var getOwner: Command { Command(F_GETOWN) } - - /// Set SIGIO/SIGURG proc/pgrp. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.setOwner(_:)`. - /// - /// TODO: `setOwner` is pending the `PIDOrPGID` type decision - /// - /// The corresponding C constant is `F_SETOWN`. - @_alwaysEmitIntoClient - public static var setOwner: Command { Command(F_SETOWN) } - /// Get open file description record locking information. /// /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html /// TODO: reference FileDesciptor.isLocked() or something like that /// /// The corresponding C constant is `F_GETLK`. - @_alwaysEmitIntoClient - public static var getOFDLock: Command { Command(_F_OFD_GETLK) } +// @_alwaysEmitIntoClient +// public + internal static var getOFDLock: Command { Command(_F_OFD_GETLK) } /// Set open file description record locking information. /// @@ -183,8 +64,9 @@ extension FileDescriptor.Control { /// TODO: reference FileDesciptor.lock() /// /// The corresponding C constant is `F_SETLK`. - @_alwaysEmitIntoClient - public static var setOFDLock: Command { Command(_F_OFD_SETLK) } +// @_alwaysEmitIntoClient +// public + internal static var setOFDLock: Command { Command(_F_OFD_SETLK) } /// Set open file description record locking information and wait until /// the request can be completed. @@ -193,8 +75,9 @@ extension FileDescriptor.Control { /// TODO: reference FileDesciptor.lock() /// /// The corresponding C constant is `F_SETLKW`. - @_alwaysEmitIntoClient - public static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } +// @_alwaysEmitIntoClient +// public + internal static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } #if !os(Linux) /// Set open file description record locking information and wait until @@ -204,431 +87,13 @@ extension FileDescriptor.Control { /// TODO: reference FileDesciptor.lock() /// /// The corresponding C constant is `F_SETLKWTIMEOUT`. - @_alwaysEmitIntoClient - public static var setOFDLockWaitTimout: Command { +// @_alwaysEmitIntoClient +// public + internal static var setOFDLockWaitTimout: Command { Command(_F_OFD_SETLKWTIMEOUT) } #endif - /// Get POSIX process-level record locking information. - /// - /// Note: This implements POSIX.1 record locking semantics. The vast - /// majority of uses are better served by either OFD locks - /// (i.e. per-`open` locks) or `flock`-style per-process locks. - /// - /// The corresponding C constant is `F_GETLK`. - @_alwaysEmitIntoClient - public static var getPOSIXProcessLock: Command { Command(F_GETLK) } - - /// Set POSIX process-level record locking information. - /// - /// Note: This implements POSIX.1 record locking semantics. The vast - /// majority of uses are better served by either OFD locks - /// (i.e. per-`open` locks) or `flock`-style per-process locks. - /// - /// The corresponding C constant is `F_SETLK`. - @_alwaysEmitIntoClient - public static var setPOSIXProcessLock: Command { Command(F_SETLK) } - - /// Set POSIX process-level record locking information and wait until the - /// request can be completed. - /// - /// Note: This implements POSIX.1 record locking semantics. The vast - /// majority of uses are better served by either OFD locks - /// (i.e. per-`open` locks) or `flock`-style per-process locks. - /// - /// The corresponding C constant is `F_SETLKW`. - @_alwaysEmitIntoClient - public static var setPOSIXProcessLockWait: Command { Command(F_SETLKW) } - - #if !os(Linux) - /// Set POSIX process-level record locking information and wait until the - /// request can be completed, returning on timeout. - /// - /// Note: This implements POSIX.1 record locking semantics. The vast - /// majority of uses are better served by either OFD locks - /// (i.e. per-`open` locks) or `flock`-style per-process locks. - /// - /// The corresponding C constant is `F_SETLKWTIMEOUT`. - @_alwaysEmitIntoClient - public static var setPOSIXProcessLockWaitTimout: Command { - Command(F_SETLKWTIMEOUT) - } - - /// ??? TODO: Where is this documented? - /// - /// The corresponding C constant is `F_FLUSH_DATA`. - @_alwaysEmitIntoClient - public static var flushData: Command { - Command(F_FLUSH_DATA) - } - - /// Used for regression test. - /// - /// The corresponding C constant is `F_CHKCLEAN`. - @_alwaysEmitIntoClient - public static var checkClean: Command { Command(F_CHKCLEAN) } - - /// Preallocate storage. - /// - /// The corresponding C constant is `F_PREALLOCATE`. - @_alwaysEmitIntoClient - public static var preallocate: Command { - Command(F_PREALLOCATE) - } - - /// Truncate a file. Equivalent to calling truncate(2). - /// - /// The corresponding C constant is `F_SETSIZE`. - @_alwaysEmitIntoClient - public static var setSize: Command { Command(F_SETSIZE) } - - /// Issue an advisory read async with no copy to user. - /// - /// The corresponding C constant is `F_RDADVISE`. - @_alwaysEmitIntoClient - public static var readAdvise: Command { Command(F_RDADVISE) } - - /// Turn read ahead off/on for this fd. - /// - /// The corresponding C constant is `F_RDAHEAD`. - @_alwaysEmitIntoClient - public static var readAhead: Command { Command(F_RDAHEAD) } - - /// - /// Header says: 46,47 used to be F_READBOOTSTRAP and F_WRITEBOOTSTRAP - /// FIXME: What do we do here? - /// - - /// Turn data caching off/on for this fd. - /// - /// The corresponding C constant is `F_NOCACHE`. - @_alwaysEmitIntoClient - public static var noCache: Command { Command(F_NOCACHE) } - - /// File offset to device offset. - /// - /// The corresponding C constant is `F_LOG2PHYS`. - @_alwaysEmitIntoClient - public static var logicalToPhysical: Command { Command(F_LOG2PHYS) } - - /// Return the full path of the fd. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. - /// - /// The corresponding C constant is `F_GETPATH`. - @_alwaysEmitIntoClient - public static var getPath: Command { Command(F_GETPATH) } - - /// Fsync + ask the drive to flush to the media. - /// - /// The corresponding C constant is `F_FULLFSYNC`. - @_alwaysEmitIntoClient - public static var fullFsync: Command { Command(F_FULLFSYNC) } - - /// Find which component (if any) is a package. - /// - /// The corresponding C constant is `F_PATHPKG_CHECK`. - @_alwaysEmitIntoClient - public static var pathPackageCheck: Command { - Command(F_PATHPKG_CHECK) - } - - /// "Freeze" all fs operations. - /// - /// The corresponding C constant is `F_FREEZE_FS`. - @_alwaysEmitIntoClient - public static var freezeFileSystem: Command { Command(F_FREEZE_FS) } - - /// "Thaw" all fs operations. - /// - /// The corresponding C constant is `F_THAW_FS`. - @_alwaysEmitIntoClient - public static var thawFileSystem: Command { Command(F_THAW_FS) } - - /// Turn data caching off/on (globally) for this file. - /// - /// The corresponding C constant is `F_GLOBAL_NOCACHE`. - @_alwaysEmitIntoClient - public static var globalNoCache: Command { - Command(F_GLOBAL_NOCACHE) - } - - /// Add detached signatures. - /// - /// The corresponding C constant is `F_ADDSIGS`. - @_alwaysEmitIntoClient - public static var addSignatures: Command { - Command(F_ADDSIGS) - } - - /// Add signature from same file (used by dyld for shared libs). - /// - /// The corresponding C constant is `F_ADDFILESIGS`. - @_alwaysEmitIntoClient - public static var addFileSignatures: Command { - Command(F_ADDFILESIGS) - } - - /// Used in conjunction with F_NOCACHE to indicate that DIRECT, - /// synchonous writes should not be used (i.e. its ok to temporaily create - /// cached pages). - /// - /// The corresponding C constant is `F_NODIRECT`. - @_alwaysEmitIntoClient - public static var noDirect: Command { Command(F_NODIRECT) } - - /// Get the protection class of a file from the EA, returns int. - /// - /// The corresponding C constant is `F_GETPROTECTIONCLASS`. - @_alwaysEmitIntoClient - public static var getProtectionClass: Command { - Command(F_GETPROTECTIONCLASS) - } - - /// Set the protection class of a file for the EA, requires int. - /// - /// The corresponding C constant is `F_SETPROTECTIONCLASS`. - @_alwaysEmitIntoClient - public static var setProtectionClass: Command { - Command(F_SETPROTECTIONCLASS) - } - - /// File offset to device offset, extended. - /// - /// The corresponding C constant is `F_LOG2PHYS_EXT`. - @_alwaysEmitIntoClient - public static var logToPhysicalExtended: Command { - Command(F_LOG2PHYS_EXT) - } - - /// Get record locking information, per-process. - /// - /// The corresponding C constant is `F_GETLKPID`. - @_alwaysEmitIntoClient - public static var getLockPID: Command { Command(F_GETLKPID) } - #endif - - /// Mark the dup with FD_CLOEXEC. - /// - /// The corresponding C constant is `F_DUPFD_CLOEXEC` - @_alwaysEmitIntoClient - public static var duplicateCloseOnExec: Command { - Command(F_DUPFD_CLOEXEC) - } - - #if !os(Linux) - /// Mark the file as being the backing store for another filesystem. - /// - /// The corresponding C constant is `F_SETBACKINGSTORE`. - @_alwaysEmitIntoClient - public static var setBackingStore: Command { - Command(F_SETBACKINGSTORE) - } - - /// Return the full path of the FD, but error in specific mtmd - /// circumstances. - /// - /// The corresponding C constant is `F_GETPATH_MTMINFO`. - @_alwaysEmitIntoClient - public static var getPathMTMDInfo: Command { - Command(F_GETPATH_MTMINFO) - } - - /// Returns the code directory, with associated hashes, to the caller. - /// - /// The corresponding C constant is `F_GETCODEDIR`. - @_alwaysEmitIntoClient - public static var getCodeDirectory: Command { - Command(F_GETCODEDIR) - } - - /// No SIGPIPE generated on EPIPE. - /// - /// The corresponding C constant is `F_SETNOSIGPIPE`. - @_alwaysEmitIntoClient - public static var setNoSigPipe: Command { - Command(F_SETNOSIGPIPE) - } - - /// Status of SIGPIPE for this fd. - /// - /// The corresponding C constant is `F_GETNOSIGPIPE`. - @_alwaysEmitIntoClient - public static var getNoSigPipe: Command { - Command(F_GETNOSIGPIPE) - } - - /// For some cases, we need to rewrap the key for AKS/MKB. - /// - /// The corresponding C constant is `F_TRANSCODEKEY`. - @_alwaysEmitIntoClient - public static var transcodeKey: Command { - Command(F_TRANSCODEKEY) - } - - /// File being written to a by single writer... if throttling enabled, - /// writes may be broken into smaller chunks with throttling in between. - /// - /// The corresponding C constant is `F_SINGLE_WRITER`. - @_alwaysEmitIntoClient - public static var singleWriter: Command { - Command(F_SINGLE_WRITER) - } - - - /// Get the protection version number for this filesystem. - /// - /// The corresponding C constant is `F_GETPROTECTIONLEVEL`. - @_alwaysEmitIntoClient - public static var getProtectionLevel: Command { - Command(F_GETPROTECTIONLEVEL) - } - - - /// Add detached code signatures (used by dyld for shared libs). - /// - /// The corresponding C constant is `F_FINDSIGS`. - @_alwaysEmitIntoClient - public static var findSignatures: Command { - Command(F_FINDSIGS) - } - - /// Add signature from same file, only if it is signed by Apple (used by - /// dyld for simulator). - /// - /// The corresponding C constant is `F_ADDFILESIGS_FOR_DYLD_SIM`. - @_alwaysEmitIntoClient - public static var addFileSignaturesForDYLDSim: Command { - Command(F_ADDFILESIGS_FOR_DYLD_SIM) - } - - /// Fsync + issue barrier to drive. - /// - /// The corresponding C constant is `F_BARRIERFSYNC`. - @_alwaysEmitIntoClient - public static var barrierFsync: Command { - Command(F_BARRIERFSYNC) - } - - /// Add signature from same file, return end offset in structure on success. - /// - /// The corresponding C constant is `F_ADDFILESIGS_RETURN`. - @_alwaysEmitIntoClient - public static var addFileSignaturesReturn: Command { - Command(F_ADDFILESIGS_RETURN) - } - - /// Check if Library Validation allows this Mach-O file to be mapped into - /// the calling process. - /// - /// The corresponding C constant is `F_CHECK_LV`. - @_alwaysEmitIntoClient - public static var checkLibraryValidation: Command { - Command(F_CHECK_LV) - } - - /// Deallocate a range of the file. - /// - /// The corresponding C constant is `F_PUNCHHOLE`. - @_alwaysEmitIntoClient - public static var punchhole: Command { Command(F_PUNCHHOLE) } - - /// Trim an active file. - /// - /// The corresponding C constant is `F_TRIM_ACTIVE_FILE`. - @_alwaysEmitIntoClient - public static var trimActiveFile: Command { - Command(F_TRIM_ACTIVE_FILE) - } - - /// Synchronous advisory read fcntl for regular and compressed file. - /// - /// The corresponding C constant is `F_SPECULATIVE_READ`. - @_alwaysEmitIntoClient - public static var speculativeRead: Command { - Command(F_SPECULATIVE_READ) - } - - /// Return the full path without firmlinks of the fd. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. - /// - /// The corresponding C constant is `F_GETPATH_NOFIRMLINK`. - @_alwaysEmitIntoClient - public static var getPathNoFirmLink: Command { - Command(F_GETPATH_NOFIRMLINK) - } - - /// Add signature from same file, return information. - /// - /// The corresponding C constant is `F_ADDFILESIGS_INFO`. - @_alwaysEmitIntoClient - public static var addFileSignatureInfo: Command { - Command(F_ADDFILESIGS_INFO) - } - - /// Add supplemental signature from same file with fd reference to original. - /// - /// The corresponding C constant is `F_ADDFILESUPPL`. - @_alwaysEmitIntoClient - public static var addFileSupplementalSignature: Command { - Command(F_ADDFILESUPPL) - } - - /// Look up code signature information attached to a file or slice. - /// - /// The corresponding C constant is `F_GETSIGSINFO`. - @_alwaysEmitIntoClient - public static var getSignatureInfo: Command { - Command(F_GETSIGSINFO) - } - #endif - - #if !os(Linux) - /// Allocate contigious space. - /// - /// TODO: This is actually a flag for the PREALLOCATE struct... - /// - /// The corresponding C constant is `F_ALLOCATECONTIG`. - @_alwaysEmitIntoClient - public static var allocateContiguous: Command { - Command(F_ALLOCATECONTIG) - } - - /// Allocate all requested space or no space at all. - /// - /// TODO: This is actually a flag for the PREALLOCATE struct... - /// - /// The corresponding C constant is `F_ALLOCATEALL`. - @_alwaysEmitIntoClient - public static var allocateAll: Command { - Command(F_ALLOCATEALL) - } - - /// Allocate from the physical end of file. - /// - /// The corresponding C constant is `F_PEOFPOSMODE`. - @_alwaysEmitIntoClient - public static var endOfFile: Command { - Command(F_PEOFPOSMODE) - } - - /// Specify volume starting postion. - /// - /// The corresponding C constant is `F_VOLPOSMODE`. - @_alwaysEmitIntoClient - public static var startOfVolume: Command { - Command(F_VOLPOSMODE) - } - #endif } } - -// - MARK: Raw escape hatch - -#if !os(Linux) -internal var _maxPathLen: Int { Int(MAXPATHLEN) } -#endif - #endif From 300188a86450a63584dfde94ab5789cdee558838 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 15 Nov 2022 15:40:03 -0700 Subject: [PATCH 12/26] wip: more cleanup --- Sources/System/FileLock.swift | 30 ------------------------------ Sources/System/ProcessID.swift | 7 ++++++- 2 files changed, 6 insertions(+), 31 deletions(-) diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index f1bbb3be..8b29575c 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -24,7 +24,6 @@ extension FileDescriptor { } extension FileDescriptor.FileLock { - /// TODO: docs @_alwaysEmitIntoClient public init() { self.init(rawValue: .init()) } @@ -66,8 +65,6 @@ extension FileDescriptor.FileLock { /// The process ID of the lock holder, filled in by`FileDescriptor.getLock()`. /// - /// TODO: Actual ProcessID type - /// /// The corresponding C field is `l_pid` @_alwaysEmitIntoClient public var pid: ProcessID { @@ -129,33 +126,6 @@ extension FileDescriptor.FileLock { } } - -// TODO: Need to version this carefully -// TODO: Don't do this, make a new type, but figure out ranges for that new type -extension FileDescriptor.SeekOrigin: Comparable, Strideable { - // TODO: Should stride be CInt or Int? - - public func distance(to other: FileDescriptor.SeekOrigin) -> Int { - Int(other.rawValue - self.rawValue) - } - - public func advanced(by n: Int) -> FileDescriptor.SeekOrigin { - .init(rawValue: self.rawValue + CInt(n)) - } - public static func < ( - lhs: FileDescriptor.SeekOrigin, rhs: FileDescriptor.SeekOrigin - ) -> Bool { - lhs.rawValue < rhs.rawValue - } -} - -extension FileDescriptor { - struct FileRange { - // Note: if it has an origin it wouldn't be comparable really or strideable - } -} - - extension FileDescriptor { /// All bytes in a file /// diff --git a/Sources/System/ProcessID.swift b/Sources/System/ProcessID.swift index 53442148..b2a2bd5a 100644 --- a/Sources/System/ProcessID.swift +++ b/Sources/System/ProcessID.swift @@ -1,5 +1,9 @@ -/// TODO: docs +#if !os(Windows) + +/// The process identifier (aka PID) used to uniquely identify an active process. +/// +/// The corresponding C type is `pid_t` @frozen public struct ProcessID: RawRepresentable, Hashable { @_alwaysEmitIntoClient @@ -9,3 +13,4 @@ public struct ProcessID: RawRepresentable, Hashable { public init(rawValue: CInterop.PID) { self.rawValue = rawValue } } +#endif From 120f8b319f29e65892d374a4bee7860c792f6074 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 15 Nov 2022 16:44:45 -0700 Subject: [PATCH 13/26] wip: drop overloads, fix docs --- Sources/System/FileLock.swift | 103 +++++++++------------------ Tests/SystemTests/FileLockTest.swift | 4 +- 2 files changed, 36 insertions(+), 71 deletions(-) diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 8b29575c..18d60ccc 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -63,7 +63,7 @@ extension FileDescriptor.FileLock { set { rawValue.l_len = CInterop.Offset(newValue) } } - /// The process ID of the lock holder, filled in by`FileDescriptor.getLock()`. + /// The process ID of the lock holder (if applicable). /// /// The corresponding C field is `l_pid` @_alwaysEmitIntoClient @@ -90,8 +90,8 @@ extension FileDescriptor.FileLock { } extension FileDescriptor.FileLock { - /// The kind of a lock: read ("shared"), write ("exclusive"), or none - /// ("unlock"). + /// The kind or type of a lock: read (aka "shared"), write (aka "exclusive"), or none + /// (aka "unlock"). @frozen public struct Kind: RawRepresentable, Hashable { @_alwaysEmitIntoClient @@ -128,13 +128,10 @@ extension FileDescriptor.FileLock { extension FileDescriptor { /// All bytes in a file - /// - /// NOTE: We can't make byteRange optional _and_ generic in our API below because that requires type inference even when passed `nil`. - /// @_alwaysEmitIntoClient internal var _allFileBytes: Range { Int64.min ..< Int64.max } - /// Get any conflicting locks held by other open file descriptions. + /// Get any conflicting locks held by other open file descriptions. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -151,24 +148,17 @@ extension FileDescriptor { /// /// - Parameters: /// - byteRange: The range of bytes over which to check for a lock. Pass - /// `nil` to consider the entire file (TODO: default value with Swift - /// 5.7) - /// - retryOnInterrupt: Whether to retry the open operation if it throws + /// `nil` to consider the entire file. + /// - retryOnInterrupt: Whether to retry the operation if it throws /// ``Errno/interrupted``. The default is `true`. Pass `false` to try /// only once and throw an error upon interruption. - /// - Returns; `.none` if there are no other locks, otherwise returns the + /// - Returns; `.none` if there are no locks, otherwise returns the /// strongest conflicting lock /// /// The corresponding C function is `fcntl` with `F_OFD_GETLK`. - /// - /// FIXME: Does this only return OFD locks or process locks too? - /// TODO: document byte-range - /// FIXME: Can we just state the OFD docs once and link to it? - /// TODO: would a better API be e.g. `canGetLock(.read)`? or `getConflictingLock()`? - /// @_alwaysEmitIntoClient public func getConflictingLock( - byteRange: some RangeExpression, + byteRange: (some RangeExpression)? = Range?.none, retryOnInterrupt: Bool = true ) throws -> FileDescriptor.FileLock.Kind { let (start, len) = _mapByteRangeToByteOffsets(byteRange) @@ -177,15 +167,6 @@ extension FileDescriptor { ).get() } - - @_alwaysEmitIntoClient - public func getConflictingLock( - retryOnInterrupt: Bool = true - ) throws -> FileDescriptor.FileLock.Kind { - try getConflictingLock( - byteRange: _allFileBytes, retryOnInterrupt: retryOnInterrupt) - } - @usableFromInline internal func _getConflictingLock( start: Int64, length: Int64, retryOnInterrupt: Bool @@ -202,14 +183,12 @@ extension FileDescriptor { // 2) Try with a write lock, which will tell us if there's either a // conflicting read or write lock in place. var lock = FileDescriptor.FileLock(ofdType: .read, start: start, length: length) -// print(lock.type) if case let .failure(err) = self._fcntl( .getOFDLock, &lock, retryOnInterrupt: retryOnInterrupt ) { return .failure(err) } if lock.type == .write { -// print(lock.type) return .success(.write) } guard lock.type == .none else { @@ -219,7 +198,6 @@ extension FileDescriptor { lock = FileDescriptor.FileLock(ofdType: .write, start: start, length: length) let secondTry = self._fcntl(.getOFDLock, &lock, retryOnInterrupt: retryOnInterrupt) -// print(lock.type) return secondTry.map { lock.type } } @@ -229,7 +207,7 @@ extension FileDescriptor { /// replaced. /// /// If the lock cannot be set because it is blocked by an existing lock on a - /// file and (TODO: blocking paremeter is false), + /// file and `wait` is `false`, /// `Errno.resourceTemporarilyUnavailable` is thrown. /// /// Open file description locks are associated with an open file @@ -248,15 +226,22 @@ extension FileDescriptor { /// Passing a lock kind of `.none` will remove a lock (equivalent to calling /// `FileDescriptor.unlock()`). /// - /// TODO: describe non-blocking + /// - Parameters: + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - wait: Whether to wait (block) until the request can be completed + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. /// /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or /// `F_OFD_SETLKW`. @_alwaysEmitIntoClient public func lock( _ kind: FileDescriptor.FileLock.Kind = .read, - byteRange: (some RangeExpression)? = nil, - nonBlocking: Bool = false, // FIXME: named "wait" or "blocking"? Which default is best? + byteRange: (some RangeExpression)? = Range?.none, + wait: Bool = false, retryOnInterrupt: Bool = true ) throws { let (start, len) = _mapByteRangeToByteOffsets(byteRange) @@ -264,25 +249,11 @@ extension FileDescriptor { kind, start: start, length: len, - nonBlocking: nonBlocking, + wait: wait, retryOnInterrupt: retryOnInterrupt ).get() } - @_alwaysEmitIntoClient - public func lock( - _ kind: FileDescriptor.FileLock.Kind = .read, - nonBlocking: Bool = false, // FIXME: named "wait" or "blocking"? Which default is best? - retryOnInterrupt: Bool = true - ) throws { - try lock( - kind, - byteRange: _allFileBytes, - nonBlocking: nonBlocking, - retryOnInterrupt: retryOnInterrupt) - } - - /// Remove an open file description lock. /// /// Open file description locks are associated with an open file @@ -301,14 +272,20 @@ extension FileDescriptor { /// Calling `unlock()` is equivalent to passing `.none` as the lock kind to /// `FileDescriptor.lock()`. /// - /// TODO: Do we need a non-blocking argument? Does that even make sense? + /// - Parameters: + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - wait: Whether to wait (block) until the request can be completed + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. /// - /// The corresponding C function is `fcntl` with `F_OFD_SETLK` (TODO: or - /// `F_OFD_SETLKW`?) and a lock type of `F_UNLCK`. + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or + /// `F_OFD_SETLKW` and a lock type of `F_UNLCK`. @_alwaysEmitIntoClient public func unlock( - byteRange: (some RangeExpression)? = nil, - nonBlocking: Bool = false, // FIXME: needed? + byteRange: (some RangeExpression)? = Range?.none, + wait: Bool = false, // FIXME: needed? retryOnInterrupt: Bool = true ) throws { let (start, len) = _mapByteRangeToByteOffsets(byteRange) @@ -316,34 +293,22 @@ extension FileDescriptor { .none, start: start, length: len, - nonBlocking: nonBlocking, + wait: wait, retryOnInterrupt: retryOnInterrupt ).get() } - @_alwaysEmitIntoClient - public func unlock( - nonBlocking: Bool = false, // FIXME: needed? - retryOnInterrupt: Bool = true - ) throws { - try unlock( - byteRange: _allFileBytes, - nonBlocking: nonBlocking, - retryOnInterrupt: retryOnInterrupt) - } - - @usableFromInline internal func _lock( _ kind: FileDescriptor.FileLock.Kind, start: Int64, length: Int64, - nonBlocking: Bool, + wait: Bool, retryOnInterrupt: Bool ) -> Result<(), Errno> { var lock = FileDescriptor.FileLock(ofdType: kind, start: start, length: length) let command: FileDescriptor.Control.Command = - nonBlocking ? .setOFDLock : .setOFDLockWait + wait ? .setOFDLockWait : .setOFDLock return _fcntl(command, &lock, retryOnInterrupt: retryOnInterrupt) } } diff --git a/Tests/SystemTests/FileLockTest.swift b/Tests/SystemTests/FileLockTest.swift index 34a1c8ad..77a00d00 100644 --- a/Tests/SystemTests/FileLockTest.swift +++ b/Tests/SystemTests/FileLockTest.swift @@ -70,12 +70,12 @@ extension FileOperationsTest { testOFDs(one: .read, two: .read) do { - try dup_2.lock(.write, nonBlocking: true) + try dup_2.lock(.write) } catch let e as Errno { XCTAssertEqual(.resourceTemporarilyUnavailable, e) } do { - try ofd_1.lock(.write, nonBlocking: true) + try ofd_1.lock(.write) } catch let e as Errno { XCTAssertEqual(.resourceTemporarilyUnavailable, e) } From d05c4b9f521feb8d5e5653ca330a3a358dedadf0 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 15 Nov 2022 16:50:48 -0700 Subject: [PATCH 14/26] wip: sendable --- Sources/System/FileLock.swift | 4 ++-- Sources/System/ProcessID.swift | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 18d60ccc..8e2b6668 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -14,7 +14,7 @@ extension FileDescriptor { /// /// The corresponding C type is `struct flock`. @frozen - public struct FileLock: RawRepresentable { + public struct FileLock: RawRepresentable, Sendable { @_alwaysEmitIntoClient public var rawValue: CInterop.FileLock @@ -93,7 +93,7 @@ extension FileDescriptor.FileLock { /// The kind or type of a lock: read (aka "shared"), write (aka "exclusive"), or none /// (aka "unlock"). @frozen - public struct Kind: RawRepresentable, Hashable { + public struct Kind: RawRepresentable, Hashable, Sendable { @_alwaysEmitIntoClient public var rawValue: CInterop.CShort diff --git a/Sources/System/ProcessID.swift b/Sources/System/ProcessID.swift index b2a2bd5a..70049a30 100644 --- a/Sources/System/ProcessID.swift +++ b/Sources/System/ProcessID.swift @@ -5,7 +5,7 @@ /// /// The corresponding C type is `pid_t` @frozen -public struct ProcessID: RawRepresentable, Hashable { +public struct ProcessID: RawRepresentable, Hashable, Sendable { @_alwaysEmitIntoClient public var rawValue: CInterop.PID From c2ccb544810462e27ffaaccf494b0b626fbea662 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Mon, 20 Feb 2023 09:42:51 -0700 Subject: [PATCH 15/26] lock/tryLock/unlock --- Sources/System/FileLock.swift | 187 +++++++++++++++++---------- Sources/System/Util.swift | 10 ++ Tests/SystemTests/FileLockTest.swift | 123 +++++++----------- 3 files changed, 175 insertions(+), 145 deletions(-) diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 8e2b6668..3c245bdf 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -127,11 +127,11 @@ extension FileDescriptor.FileLock { } extension FileDescriptor { - /// All bytes in a file - @_alwaysEmitIntoClient - internal var _allFileBytes: Range { Int64.min ..< Int64.max } - - /// Get any conflicting locks held by other open file descriptions. + /// Set an advisory open file description lock. + /// + /// If the open file description already has a lock, the old lock is + /// replaced. If the lock cannot be set because it is blocked by an existing lock, + /// this will wait until the lock can be set. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -146,69 +146,92 @@ extension FileDescriptor { /// Open file description locks are inherited by child processes across /// `fork`, etc. /// + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). + /// /// - Parameters: - /// - byteRange: The range of bytes over which to check for a lock. Pass + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass /// `nil` to consider the entire file. /// - retryOnInterrupt: Whether to retry the operation if it throws /// ``Errno/interrupted``. The default is `true`. Pass `false` to try /// only once and throw an error upon interruption. - /// - Returns; `.none` if there are no locks, otherwise returns the - /// strongest conflicting lock /// - /// The corresponding C function is `fcntl` with `F_OFD_GETLK`. + /// The corresponding C function is `fcntl` with `F_OFD_SETLKW`. @_alwaysEmitIntoClient - public func getConflictingLock( + public func lock( + _ kind: FileDescriptor.FileLock.Kind = .read, byteRange: (some RangeExpression)? = Range?.none, retryOnInterrupt: Bool = true - ) throws -> FileDescriptor.FileLock.Kind { + ) throws { let (start, len) = _mapByteRangeToByteOffsets(byteRange) - return try _getConflictingLock( - start: start, length: len, retryOnInterrupt: retryOnInterrupt + try _lock( + kind, + start: start, + length: len, + retryOnInterrupt: retryOnInterrupt ).get() } - @usableFromInline - internal func _getConflictingLock( - start: Int64, length: Int64, retryOnInterrupt: Bool - ) -> Result { - // If there are multiple locks already in place on a file region, the lock that - // is returned is unspecified. E.g. there could be a write lock over one - // portion of the file and a read lock over another overlapping - // region. Thus, we first check if there are any write locks, and if not - // we issue another call to check for any reads-or-writes. - // - // 1) Try with a read lock, which will tell us if there's a conflicting - // write lock in place. - // - // 2) Try with a write lock, which will tell us if there's either a - // conflicting read or write lock in place. - var lock = FileDescriptor.FileLock(ofdType: .read, start: start, length: length) - if case let .failure(err) = self._fcntl( - .getOFDLock, &lock, retryOnInterrupt: retryOnInterrupt - ) { - return .failure(err) - } - if lock.type == .write { - return .success(.write) - } - guard lock.type == .none else { - fatalError("FIXME: really shouldn't be possible") + /// Try to set an advisory open file description lock. + /// + /// If the open file description already has a lock, the old lock is + /// replaced. If the lock cannot be set because it is blocked by an existing lock, + /// that is if the syscall would throw `.resourceTemporarilyUnavailable` + /// (aka `EAGAIN`), this will return `false`. + /// + /// Open file description locks are associated with an open file + /// description (see `FileDescriptor.open`). Duplicated + /// file descriptors (see `FileDescriptor.duplicate`) share open file + /// description locks. + /// + /// Locks are advisory, which allow cooperating code to perform + /// consistent operations on files, but do not guarantee consistency. + /// (i.e. other code may still access files without using advisory locks + /// possibly resulting in inconsistencies). + /// + /// Open file description locks are inherited by child processes across + /// `fork`, etc. + /// + /// Passing a lock kind of `.none` will remove a lock (equivalent to calling + /// `FileDescriptor.unlock()`). + /// + /// - Parameters: + /// - kind: The kind of lock to set + /// - byteRange: The range of bytes over which to lock. Pass + /// `nil` to consider the entire file. + /// - retryOnInterrupt: Whether to retry the operation if it throws + /// ``Errno/interrupted``. The default is `true`. Pass `false` to try + /// only once and throw an error upon interruption. + /// - Returns: `true` if the lock was aquired, `false` otherwise + /// + /// The corresponding C function is `fcntl` with `F_OFD_SETLK`. + @_alwaysEmitIntoClient + public func tryLock( + _ kind: FileDescriptor.FileLock.Kind = .read, + byteRange: (some RangeExpression)? = Range?.none, + retryOnInterrupt: Bool = true + ) throws -> Bool { + let (start, len) = _mapByteRangeToByteOffsets(byteRange) + guard let _ = try _tryLock( + kind, + waitUntilTimeout: false, + start: start, + length: len, + retryOnInterrupt: retryOnInterrupt + )?.get() else { + return false } - // This means there was no conflicting lock, so try to detect reads - lock = FileDescriptor.FileLock(ofdType: .write, start: start, length: length) - - let secondTry = self._fcntl(.getOFDLock, &lock, retryOnInterrupt: retryOnInterrupt) - return secondTry.map { lock.type } + return true } - /// Set an open file description lock. + #if !os(Linux) + /// Try to set an advisory open file description lock. /// /// If the open file description already has a lock, the old lock is - /// replaced. - /// - /// If the lock cannot be set because it is blocked by an existing lock on a - /// file and `wait` is `false`, - /// `Errno.resourceTemporarilyUnavailable` is thrown. + /// replaced. If the lock cannot be set because it is blocked by an existing lock, + /// that is if the syscall would throw `.resourceTemporarilyUnavailable` + /// (aka `EAGAIN`), this will return `false`. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -230,29 +253,33 @@ extension FileDescriptor { /// - kind: The kind of lock to set /// - byteRange: The range of bytes over which to lock. Pass /// `nil` to consider the entire file. - /// - wait: Whether to wait (block) until the request can be completed + /// - waitUntilTimeout: If `true`, will wait until a timeout (determined by the operating system) /// - retryOnInterrupt: Whether to retry the operation if it throws /// ``Errno/interrupted``. The default is `true`. Pass `false` to try /// only once and throw an error upon interruption. + /// - Returns: `true` if the lock was aquired, `false` otherwise /// - /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or - /// `F_OFD_SETLKW`. + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_OFD_SETLKWTIMEOUT` . @_alwaysEmitIntoClient - public func lock( + public func tryLock( _ kind: FileDescriptor.FileLock.Kind = .read, byteRange: (some RangeExpression)? = Range?.none, - wait: Bool = false, + waitUntilTimeout: Bool, retryOnInterrupt: Bool = true - ) throws { + ) throws -> Bool { let (start, len) = _mapByteRangeToByteOffsets(byteRange) - try _lock( + guard let _ = try _tryLock( kind, + waitUntilTimeout: waitUntilTimeout, start: start, length: len, - wait: wait, retryOnInterrupt: retryOnInterrupt - ).get() + )?.get() else { + return false + } + return true } + #endif /// Remove an open file description lock. /// @@ -275,7 +302,6 @@ extension FileDescriptor { /// - Parameters: /// - byteRange: The range of bytes over which to lock. Pass /// `nil` to consider the entire file. - /// - wait: Whether to wait (block) until the request can be completed /// - retryOnInterrupt: Whether to retry the operation if it throws /// ``Errno/interrupted``. The default is `true`. Pass `false` to try /// only once and throw an error upon interruption. @@ -289,13 +315,16 @@ extension FileDescriptor { retryOnInterrupt: Bool = true ) throws { let (start, len) = _mapByteRangeToByteOffsets(byteRange) - try _lock( + guard let res = _tryLock( .none, + waitUntilTimeout: false, // TODO: or we wait for timeout? start: start, length: len, - wait: wait, retryOnInterrupt: retryOnInterrupt - ).get() + ) else { + preconditionFailure("TODO: Unlock should always succeed?") + } + return try res.get() } @usableFromInline @@ -303,13 +332,35 @@ extension FileDescriptor { _ kind: FileDescriptor.FileLock.Kind, start: Int64, length: Int64, - wait: Bool, retryOnInterrupt: Bool ) -> Result<(), Errno> { - var lock = FileDescriptor.FileLock(ofdType: kind, start: start, length: length) - let command: FileDescriptor.Control.Command = - wait ? .setOFDLockWait : .setOFDLock - return _fcntl(command, &lock, retryOnInterrupt: retryOnInterrupt) + var lock = FileDescriptor.FileLock( + ofdType: kind, start: start, length: length) + return _fcntl(.setOFDLockWait, &lock, retryOnInterrupt: retryOnInterrupt) + } + + @usableFromInline + internal func _tryLock( + _ kind: FileDescriptor.FileLock.Kind, + waitUntilTimeout: Bool, + start: Int64, + length: Int64, + retryOnInterrupt: Bool + ) -> Result<(), Errno>? { +#if os(Linux) + precondition(!waitUntilTimeout, "`waitUntilTimeout` unavailable on Linux") +#endif + + let cmd: Control.Command + if waitUntilTimeout { + cmd = .setOFDLockWaitTimout + } else { + cmd = .setOFDLock + } + var lock = FileDescriptor.FileLock( + ofdType: kind, start: start, length: length) + return _extractWouldBlock( + _fcntl(cmd, &lock, retryOnInterrupt: retryOnInterrupt)) } } #endif diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index a977eaab..a4782f10 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -43,6 +43,16 @@ internal func nothingOrErrno( valueOrErrno(retryOnInterrupt: retryOnInterrupt, f).map { _ in () } } +/// Promote `Errno.wouldBlcok` to `nil`. +internal func _extractWouldBlock( + _ value: Result +) -> Result? { + if case .failure(let err) = value, err == .wouldBlock { + return nil + } + return value +} + // Run a precondition for debug client builds internal func _debugPrecondition( _ condition: @autoclosure () -> Bool, diff --git a/Tests/SystemTests/FileLockTest.swift b/Tests/SystemTests/FileLockTest.swift index 77a00d00..0abff10d 100644 --- a/Tests/SystemTests/FileLockTest.swift +++ b/Tests/SystemTests/FileLockTest.swift @@ -20,93 +20,62 @@ func _range(_ r: some RangeExpression) -> Range { } extension FileOperationsTest { - func testFileLocks() throws { let path = FilePath("/tmp/\(UUID().uuidString).txt") - let ofd_1 = try FileDescriptor.open( + let ofd_A = try FileDescriptor.open( path, .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) - let dup_1 = try ofd_1.duplicate() + let dup_A = try ofd_A.duplicate() - let ofd_2 = try FileDescriptor.open( + let ofd_B = try FileDescriptor.open( path, .readWrite, options: [.create, .truncate], permissions: .ownerReadWrite) - let dup_2 = try ofd_2.duplicate() - - func testOFDs( - one: FileDescriptor.FileLock.Kind, - two: FileDescriptor.FileLock.Kind, - byteRange: Range? = nil - ) { - if let br = byteRange { - XCTAssertEqual(one, try ofd_1.getConflictingLock(byteRange: br)) - XCTAssertEqual(one, try dup_1.getConflictingLock(byteRange: br)) - - XCTAssertEqual(two, try ofd_2.getConflictingLock(byteRange: br)) - XCTAssertEqual(two, try dup_2.getConflictingLock(byteRange: br)) - } else { - XCTAssertEqual(one, try ofd_1.getConflictingLock()) - XCTAssertEqual(one, try dup_1.getConflictingLock()) - - XCTAssertEqual(two, try ofd_2.getConflictingLock()) - XCTAssertEqual(two, try dup_2.getConflictingLock()) - } - } - - testOFDs(one: .none, two: .none) - - try ofd_1.lock() - testOFDs(one: .none, two: .read) - - try ofd_1.lock(.write) - testOFDs(one: .none, two: .write) - - try dup_1.unlock() - testOFDs(one: .none, two: .none) - - try dup_2.lock() - testOFDs(one: .read, two: .none) - - try dup_1.lock() - testOFDs(one: .read, two: .read) - - do { - try dup_2.lock(.write) - } catch let e as Errno { - XCTAssertEqual(.resourceTemporarilyUnavailable, e) - } - do { - try ofd_1.lock(.write) - } catch let e as Errno { - XCTAssertEqual(.resourceTemporarilyUnavailable, e) - } - - try ofd_1.unlock() - try ofd_2.unlock() - testOFDs(one: .none, two: .none) + let dup_B = try ofd_B.duplicate() + + // A(read) -> A(write) -> FAIL: B(read/write) + XCTAssertTrue(try ofd_A.tryLock(.read)) + XCTAssertTrue(try ofd_A.tryLock(.write)) + XCTAssertTrue(try dup_A.tryLock(.write)) // redundant, but works + XCTAssertFalse(try ofd_B.tryLock(.read)) + XCTAssertFalse(try ofd_B.tryLock(.write)) + XCTAssertFalse(try dup_B.tryLock(.write)) + try dup_A.unlock() + + // A(read) -> B(read) -> FAIL: A/B(write) + // -> B(unlock) -> A(write) -> FAIL: B(read/write) + XCTAssertTrue(try dup_A.tryLock(.read)) + XCTAssertTrue(try ofd_B.tryLock(.read)) + XCTAssertFalse(try ofd_A.tryLock(.write)) + XCTAssertFalse(try dup_A.tryLock(.write)) + XCTAssertFalse(try ofd_B.tryLock(.write)) + XCTAssertFalse(try dup_B.tryLock(.write)) + try dup_B.unlock() + XCTAssertTrue(try ofd_A.tryLock(.write)) + XCTAssertFalse(try dup_B.tryLock(.read)) + XCTAssertFalse(try ofd_B.tryLock(.write)) + try dup_A.unlock() /// Byte ranges - try dup_1.lock(byteRange: ..<50) - testOFDs(one: .none, two: .read) - testOFDs(one: .none, two: .none, byteRange: _range(51...)) - testOFDs(one: .none, two: .read, byteRange: _range(1..<2)) - - try dup_1.lock(.write, byteRange: 100..<150) - testOFDs(one: .none, two: .write) - testOFDs(one: .none, two: .read, byteRange: 49..<50) - testOFDs(one: .none, two: .none, byteRange: 98..<99) - testOFDs(one: .none, two: .write, byteRange: _range(100...)) - - try dup_1.unlock(byteRange: ..<49) - testOFDs(one: .none, two: .read, byteRange: 49..<50) - - try dup_1.unlock(byteRange: ..<149) - testOFDs(one: .none, two: .write) - testOFDs(one: .none, two: .none, byteRange: _range(..<149)) - testOFDs(one: .none, two: .write, byteRange: 149..<150) + // A(read, ..<50) -> B(write, 50...) + // -> A(write, 10..<20) -> B(read, 40..<50) + // -> FAIL: B(read, 17..<18), A(read 60..<70) + // -> A(unlock, 11..<12) -> B(read, 11..<12) -> A(read, 11..<12) + // -> FAIL A/B(write, 11..<12) + XCTAssertTrue(try ofd_A.tryLock(.read, byteRange: ..<50)) + XCTAssertTrue(try ofd_B.tryLock(.write, byteRange: 50...)) + XCTAssertTrue(try ofd_A.tryLock(.write, byteRange: 10..<20)) + XCTAssertTrue(try ofd_B.tryLock(.read, byteRange: 40..<50)) + XCTAssertFalse(try ofd_B.tryLock(.read, byteRange: 17..<18)) + XCTAssertFalse(try ofd_A.tryLock(.read, byteRange: 60..<70)) + try dup_A.unlock(byteRange: 11..<12) + XCTAssertTrue(try ofd_B.tryLock(.read, byteRange: 11..<12)) + XCTAssertTrue(try ofd_A.tryLock(.read, byteRange: 11..<12)) + XCTAssertFalse(try ofd_B.tryLock(.write, byteRange: 11..<12)) + XCTAssertFalse(try ofd_A.tryLock(.write, byteRange: 11..<12)) + } - try dup_1.unlock(byteRange: 149..<150) - testOFDs(one: .none, two: .none) + func testFileLocksWaiting() { + // TODO: Test waiting, test waiting until timeouts } } From a1cbeeb076dbd61076b5e85a96b2160aadc74f92 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 7 Mar 2023 17:04:04 -0700 Subject: [PATCH 16/26] lock and lock(wait:) --- Sources/System/FileControl.swift | 2 +- Sources/System/FileControlRaw.swift | 41 +------ Sources/System/FileLock.swift | 126 +++++++++++---------- Sources/System/Internals/CInterop.swift | 2 +- Sources/System/Util.swift | 35 +----- Tests/SystemTests/FileLockTest.swift | 50 ++++---- Tests/SystemTests/FileOperationsTest.swift | 21 ---- 7 files changed, 105 insertions(+), 172 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index 6b5e050c..eefb5f4b 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -12,7 +12,7 @@ extension FileDescriptor { internal func _fcntl( - _ cmd: Control.Command, _ lock: inout FileDescriptor.FileLock, + _ cmd: Command, _ lock: inout FileDescriptor.FileLock, retryOnInterrupt: Bool ) -> Result<(), Errno> { nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 61f134ba..79eedf02 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -20,75 +20,40 @@ import Glibc #if !os(Windows) -extension FileDescriptor { - /// A namespace for types and values for `FileDescriptor.control()`, aka `fcntl`. - /// - /// TODO: a better name? "Internals", "Raw", "FCNTL"? I feel like a - /// precedent would be useful for sysctl, ioctl, and other grab-bag - /// things. "junk drawer" can be an anti-pattern, but is better than - /// trashing the higher namespace. - // public - internal enum Control {} - -} // - MARK: Commands -extension FileDescriptor.Control { +// TODO: Make below API as part of broader `fcntl` support. +extension FileDescriptor { /// Commands (and various constants) to pass to `fcntl`. - // @frozen - // public internal struct Command: RawRepresentable, Hashable { -// @_alwaysEmitIntoClient -// public internal let rawValue: CInt -// @_alwaysEmitIntoClient -// public internal init(rawValue: CInt) { self.rawValue = rawValue } @_alwaysEmitIntoClient private init(_ raw: CInt) { self.init(rawValue: raw) } + /// Get open file description record locking information. /// - /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html - /// TODO: reference FileDesciptor.isLocked() or something like that - /// /// The corresponding C constant is `F_GETLK`. -// @_alwaysEmitIntoClient -// public internal static var getOFDLock: Command { Command(_F_OFD_GETLK) } /// Set open file description record locking information. /// - /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html - /// TODO: reference FileDesciptor.lock() - /// /// The corresponding C constant is `F_SETLK`. -// @_alwaysEmitIntoClient -// public internal static var setOFDLock: Command { Command(_F_OFD_SETLK) } /// Set open file description record locking information and wait until /// the request can be completed. /// - /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html - /// TODO: reference FileDesciptor.lock() - /// /// The corresponding C constant is `F_SETLKW`. -// @_alwaysEmitIntoClient -// public internal static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } #if !os(Linux) /// Set open file description record locking information and wait until /// the request can be completed, returning on timeout. /// - /// TODO: link to https://www.gnu.org/software/libc/manual/html_node/Open-File-Description-Locks.html - /// TODO: reference FileDesciptor.lock() - /// /// The corresponding C constant is `F_SETLKWTIMEOUT`. -// @_alwaysEmitIntoClient -// public internal static var setOFDLockWaitTimout: Command { Command(_F_OFD_SETLKWTIMEOUT) } diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 3c245bdf..4119e40a 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -123,6 +123,18 @@ extension FileDescriptor.FileLock { public static var none: Self { Self(rawValue: CInterop.CShort(truncatingIfNeeded: F_UNLCK)) } + + /// Shared (alias for `read`) + @_alwaysEmitIntoClient + public static var shared: Self { .read } + + /// Exclusive (alias for `write`) + @_alwaysEmitIntoClient + public static var exclusive: Self { .write } + + /// Unlock (alias for `none`) + @_alwaysEmitIntoClient + public static var unlock: Self { .none } } } @@ -131,7 +143,8 @@ extension FileDescriptor { /// /// If the open file description already has a lock, the old lock is /// replaced. If the lock cannot be set because it is blocked by an existing lock, - /// this will wait until the lock can be set. + /// that is if the syscall would throw `.resourceTemporarilyUnavailable` + /// (aka `EAGAIN`), this will return `false`. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -156,29 +169,31 @@ extension FileDescriptor { /// - retryOnInterrupt: Whether to retry the operation if it throws /// ``Errno/interrupted``. The default is `true`. Pass `false` to try /// only once and throw an error upon interruption. + /// - Returns: `true` if the lock was aquired, `false` otherwise /// - /// The corresponding C function is `fcntl` with `F_OFD_SETLKW`. + /// The corresponding C function is `fcntl` with `F_OFD_SETLK`. @_alwaysEmitIntoClient public func lock( _ kind: FileDescriptor.FileLock.Kind = .read, byteRange: (some RangeExpression)? = Range?.none, retryOnInterrupt: Bool = true - ) throws { + ) throws -> Bool { let (start, len) = _mapByteRangeToByteOffsets(byteRange) - try _lock( + return try _lock( kind, start: start, length: len, + wait: false, + waitUntilTimeout: false, retryOnInterrupt: retryOnInterrupt - ).get() + )?.get() != nil } - /// Try to set an advisory open file description lock. + /// Set an advisory open file description lock. /// /// If the open file description already has a lock, the old lock is - /// replaced. If the lock cannot be set because it is blocked by an existing lock, - /// that is if the syscall would throw `.resourceTemporarilyUnavailable` - /// (aka `EAGAIN`), this will return `false`. + /// replaced. If the lock cannot be set because it is blocked by an existing lock and + /// `wait` is true, this will wait until the lock can be set, otherwise returns `false`. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -200,38 +215,38 @@ extension FileDescriptor { /// - kind: The kind of lock to set /// - byteRange: The range of bytes over which to lock. Pass /// `nil` to consider the entire file. + /// - wait: if `true` will wait until the lock can be set /// - retryOnInterrupt: Whether to retry the operation if it throws /// ``Errno/interrupted``. The default is `true`. Pass `false` to try /// only once and throw an error upon interruption. - /// - Returns: `true` if the lock was aquired, `false` otherwise /// - /// The corresponding C function is `fcntl` with `F_OFD_SETLK`. + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_OFD_SETLKW`. + @discardableResult @_alwaysEmitIntoClient - public func tryLock( + public func lock( _ kind: FileDescriptor.FileLock.Kind = .read, byteRange: (some RangeExpression)? = Range?.none, + wait: Bool, retryOnInterrupt: Bool = true ) throws -> Bool { let (start, len) = _mapByteRangeToByteOffsets(byteRange) - guard let _ = try _tryLock( + return try _lock( kind, - waitUntilTimeout: false, start: start, length: len, + wait: wait, + waitUntilTimeout: false, retryOnInterrupt: retryOnInterrupt - )?.get() else { - return false - } - return true + )?.get() != nil } - #if !os(Linux) - /// Try to set an advisory open file description lock. +#if !os(Linux) + /// Set an advisory open file description lock. /// /// If the open file description already has a lock, the old lock is - /// replaced. If the lock cannot be set because it is blocked by an existing lock, - /// that is if the syscall would throw `.resourceTemporarilyUnavailable` - /// (aka `EAGAIN`), this will return `false`. + /// replaced. If the lock cannot be set because it is blocked by an existing lock and + /// `waitUntilTimeout` is true, this will wait until the lock can be set (or the operating + /// system's timeout expires), otherwise returns `false`. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -253,33 +268,30 @@ extension FileDescriptor { /// - kind: The kind of lock to set /// - byteRange: The range of bytes over which to lock. Pass /// `nil` to consider the entire file. - /// - waitUntilTimeout: If `true`, will wait until a timeout (determined by the operating system) + /// - waitUntilTimeout: if `true` will wait until the lock can be set or a timeout expires /// - retryOnInterrupt: Whether to retry the operation if it throws /// ``Errno/interrupted``. The default is `true`. Pass `false` to try /// only once and throw an error upon interruption. - /// - Returns: `true` if the lock was aquired, `false` otherwise /// - /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_OFD_SETLKWTIMEOUT` . + /// The corresponding C function is `fcntl` with `F_OFD_SETLK` or `F_SETLKWTIMEOUT`. @_alwaysEmitIntoClient - public func tryLock( + public func lock( _ kind: FileDescriptor.FileLock.Kind = .read, byteRange: (some RangeExpression)? = Range?.none, waitUntilTimeout: Bool, retryOnInterrupt: Bool = true ) throws -> Bool { let (start, len) = _mapByteRangeToByteOffsets(byteRange) - guard let _ = try _tryLock( + return try _lock( kind, - waitUntilTimeout: waitUntilTimeout, start: start, length: len, + wait: false, + waitUntilTimeout: waitUntilTimeout, retryOnInterrupt: retryOnInterrupt - )?.get() else { - return false - } - return true + )?.get() != nil } - #endif +#endif /// Remove an open file description lock. /// @@ -311,49 +323,48 @@ extension FileDescriptor { @_alwaysEmitIntoClient public func unlock( byteRange: (some RangeExpression)? = Range?.none, - wait: Bool = false, // FIXME: needed? retryOnInterrupt: Bool = true ) throws { let (start, len) = _mapByteRangeToByteOffsets(byteRange) - guard let res = _tryLock( + guard try _lock( .none, - waitUntilTimeout: false, // TODO: or we wait for timeout? start: start, length: len, + wait: false, + waitUntilTimeout: false, retryOnInterrupt: retryOnInterrupt - ) else { - preconditionFailure("TODO: Unlock should always succeed?") + )?.get() != nil else { + // NOTE: Errno and syscall composition wasn't designed for the modern + // world. Releasing locks should always succeed and never be blocked + // by an existing lock held elsewhere. But there's always a chance + // that some effect (e.g. from NFS) causes `EGAIN` to be thrown for a + // different reason/purpose. Here, in the very unlikely situation + // that we somehow saw it, we convert the `nil` back to the error. + throw Errno.resourceTemporarilyUnavailable } - return try res.get() } + /// Internal lock entry point, returns `nil` if blocked by existing lock. + /// Both `wait` and `waitUntilTimeout` cannot both be true (passed as bools to avoid + /// spurious enum in the ABI). @usableFromInline internal func _lock( _ kind: FileDescriptor.FileLock.Kind, start: Int64, length: Int64, - retryOnInterrupt: Bool - ) -> Result<(), Errno> { - var lock = FileDescriptor.FileLock( - ofdType: kind, start: start, length: length) - return _fcntl(.setOFDLockWait, &lock, retryOnInterrupt: retryOnInterrupt) - } - - @usableFromInline - internal func _tryLock( - _ kind: FileDescriptor.FileLock.Kind, + wait: Bool, waitUntilTimeout: Bool, - start: Int64, - length: Int64, retryOnInterrupt: Bool ) -> Result<(), Errno>? { + precondition(!wait || !waitUntilTimeout) + let cmd: FileDescriptor.Command + if waitUntilTimeout { #if os(Linux) - precondition(!waitUntilTimeout, "`waitUntilTimeout` unavailable on Linux") + preconditionFailure("`waitUntilTimeout` unavailable on Linux") #endif - - let cmd: Control.Command - if waitUntilTimeout { cmd = .setOFDLockWaitTimout + } else if wait { + cmd = .setOFDLockWait } else { cmd = .setOFDLock } @@ -363,5 +374,6 @@ extension FileDescriptor { _fcntl(cmd, &lock, retryOnInterrupt: retryOnInterrupt)) } } -#endif + +#endif // !os(Windows) diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index 8f4b7119..0f7d8dca 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -70,7 +70,6 @@ public enum CInterop { #if !os(Windows) /// The C `struct flock` type public typealias FileLock = flock -#endif /// The C `pid_t` type public typealias PID = pid_t @@ -81,6 +80,7 @@ public enum CInterop { /// might otherwise appear. This typealias allows conversion code to be /// emitted into client. public typealias Offset = off_t +#endif } diff --git a/Sources/System/Util.swift b/Sources/System/Util.swift index a4782f10..56f4344e 100644 --- a/Sources/System/Util.swift +++ b/Sources/System/Util.swift @@ -43,7 +43,7 @@ internal func nothingOrErrno( valueOrErrno(retryOnInterrupt: retryOnInterrupt, f).map { _ in () } } -/// Promote `Errno.wouldBlcok` to `nil`. +/// Promote `Errno.wouldBlock` / `Errno.resourceTemporarilyUnavailable` to `nil`. internal func _extractWouldBlock( _ value: Result ) -> Result? { @@ -166,32 +166,9 @@ internal func _mapByteRangeToByteOffsets( let start = br.lowerBound == Int64.min ? 0 : br.lowerBound - let len: Int64 = { - if br.upperBound == Int64.max { - // l_len == 0 means until end of file - return 0 - } - return br.upperBound - start - }() - return (start, len) - - /* - - let len: Int64 = { - if byteRange.upperBound == Int64.max { - // l_len == 0 means until end of file - 0 - } else { - byteRange.upperBound - start - } - }() - - let len = if byteRange.upperbound == Int64.max { - // l_len == 0 means until end of file - 0 - } else { - byteRange.upperBound - start - } - - */ + if br.upperBound == Int64.max { + // l_len == 0 means until end of file + return (start, 0) + } + return (start, br.upperBound - start) } diff --git a/Tests/SystemTests/FileLockTest.swift b/Tests/SystemTests/FileLockTest.swift index 0abff10d..35e4ef75 100644 --- a/Tests/SystemTests/FileLockTest.swift +++ b/Tests/SystemTests/FileLockTest.swift @@ -32,26 +32,26 @@ extension FileOperationsTest { let dup_B = try ofd_B.duplicate() // A(read) -> A(write) -> FAIL: B(read/write) - XCTAssertTrue(try ofd_A.tryLock(.read)) - XCTAssertTrue(try ofd_A.tryLock(.write)) - XCTAssertTrue(try dup_A.tryLock(.write)) // redundant, but works - XCTAssertFalse(try ofd_B.tryLock(.read)) - XCTAssertFalse(try ofd_B.tryLock(.write)) - XCTAssertFalse(try dup_B.tryLock(.write)) + XCTAssertTrue(try ofd_A.lock(.read)) + XCTAssertTrue(try ofd_A.lock(.write)) + XCTAssertTrue(try dup_A.lock(.write)) // redundant, but works + XCTAssertFalse(try ofd_B.lock(.read)) + XCTAssertFalse(try ofd_B.lock(.write)) + XCTAssertFalse(try dup_B.lock(.write)) try dup_A.unlock() // A(read) -> B(read) -> FAIL: A/B(write) // -> B(unlock) -> A(write) -> FAIL: B(read/write) - XCTAssertTrue(try dup_A.tryLock(.read)) - XCTAssertTrue(try ofd_B.tryLock(.read)) - XCTAssertFalse(try ofd_A.tryLock(.write)) - XCTAssertFalse(try dup_A.tryLock(.write)) - XCTAssertFalse(try ofd_B.tryLock(.write)) - XCTAssertFalse(try dup_B.tryLock(.write)) + XCTAssertTrue(try dup_A.lock(.read)) + XCTAssertTrue(try ofd_B.lock(.read)) + XCTAssertFalse(try ofd_A.lock(.write)) + XCTAssertFalse(try dup_A.lock(.write)) + XCTAssertFalse(try ofd_B.lock(.write)) + XCTAssertFalse(try dup_B.lock(.write)) try dup_B.unlock() - XCTAssertTrue(try ofd_A.tryLock(.write)) - XCTAssertFalse(try dup_B.tryLock(.read)) - XCTAssertFalse(try ofd_B.tryLock(.write)) + XCTAssertTrue(try ofd_A.lock(.write)) + XCTAssertFalse(try dup_B.lock(.read)) + XCTAssertFalse(try ofd_B.lock(.write)) try dup_A.unlock() /// Byte ranges @@ -61,17 +61,17 @@ extension FileOperationsTest { // -> FAIL: B(read, 17..<18), A(read 60..<70) // -> A(unlock, 11..<12) -> B(read, 11..<12) -> A(read, 11..<12) // -> FAIL A/B(write, 11..<12) - XCTAssertTrue(try ofd_A.tryLock(.read, byteRange: ..<50)) - XCTAssertTrue(try ofd_B.tryLock(.write, byteRange: 50...)) - XCTAssertTrue(try ofd_A.tryLock(.write, byteRange: 10..<20)) - XCTAssertTrue(try ofd_B.tryLock(.read, byteRange: 40..<50)) - XCTAssertFalse(try ofd_B.tryLock(.read, byteRange: 17..<18)) - XCTAssertFalse(try ofd_A.tryLock(.read, byteRange: 60..<70)) + XCTAssertTrue(try ofd_A.lock(.read, byteRange: ..<50)) + XCTAssertTrue(try ofd_B.lock(.write, byteRange: 50...)) + XCTAssertTrue(try ofd_A.lock(.write, byteRange: 10..<20)) + XCTAssertTrue(try ofd_B.lock(.read, byteRange: 40..<50)) + XCTAssertFalse(try ofd_B.lock(.read, byteRange: 17..<18)) + XCTAssertFalse(try ofd_A.lock(.read, byteRange: 60..<70)) try dup_A.unlock(byteRange: 11..<12) - XCTAssertTrue(try ofd_B.tryLock(.read, byteRange: 11..<12)) - XCTAssertTrue(try ofd_A.tryLock(.read, byteRange: 11..<12)) - XCTAssertFalse(try ofd_B.tryLock(.write, byteRange: 11..<12)) - XCTAssertFalse(try ofd_A.tryLock(.write, byteRange: 11..<12)) + XCTAssertTrue(try ofd_B.lock(.read, byteRange: 11..<12)) + XCTAssertTrue(try ofd_A.lock(.read, byteRange: 11..<12)) + XCTAssertFalse(try ofd_B.lock(.write, byteRange: 11..<12)) + XCTAssertFalse(try ofd_A.lock(.write, byteRange: 11..<12)) } func testFileLocksWaiting() { diff --git a/Tests/SystemTests/FileOperationsTest.swift b/Tests/SystemTests/FileOperationsTest.swift index 14908f08..5591d01a 100644 --- a/Tests/SystemTests/FileOperationsTest.swift +++ b/Tests/SystemTests/FileOperationsTest.swift @@ -83,27 +83,6 @@ final class FileOperationsTest: XCTestCase { }, ] -// #if !os(Windows) -// syscallTestCases.append(contentsOf: [ -// // 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() } } From 015ba248da1562ed103855b96c7f30e998baab1b Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 7 Mar 2023 17:57:03 -0700 Subject: [PATCH 17/26] workaround SIL verifier --- Sources/System/FileControlRaw.swift | 1 - 1 file changed, 1 deletion(-) diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 79eedf02..5a98e2a8 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -30,7 +30,6 @@ extension FileDescriptor { internal init(rawValue: CInt) { self.rawValue = rawValue } - @_alwaysEmitIntoClient private init(_ raw: CInt) { self.init(rawValue: raw) } /// Get open file description record locking information. From 9b3d73f7aa08e6834011745a3178021d0a27b7e8 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 7 Mar 2023 18:03:44 -0700 Subject: [PATCH 18/26] fix linux build --- Sources/System/FileLock.swift | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 4119e40a..07093139 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -361,8 +361,10 @@ extension FileDescriptor { if waitUntilTimeout { #if os(Linux) preconditionFailure("`waitUntilTimeout` unavailable on Linux") -#endif + cmd = .setOFDLock +#else cmd = .setOFDLockWaitTimout +#endif } else if wait { cmd = .setOFDLockWait } else { From e8eb2ed5b21eb0a7a7a331b345d14d33aa62d989 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Tue, 7 Mar 2023 18:06:56 -0700 Subject: [PATCH 19/26] Update test manifest for linux --- Tests/SystemTests/XCTestManifests.swift | 25 +++++++++++++++++++++++-- 1 file changed, 23 insertions(+), 2 deletions(-) diff --git a/Tests/SystemTests/XCTestManifests.swift b/Tests/SystemTests/XCTestManifests.swift index a4f4205a..b096f265 100644 --- a/Tests/SystemTests/XCTestManifests.swift +++ b/Tests/SystemTests/XCTestManifests.swift @@ -28,8 +28,8 @@ extension FileOperationsTest { static let __allTests__FileOperationsTest = [ ("testAdHocOpen", testAdHocOpen), ("testAdHocPipe", testAdHocPipe), - ("testFcntl", testFcntl), - ("testFlock", testFlock), + ("testFileLocks", testFileLocks), + ("testFileLocksWaiting", testFileLocksWaiting), ("testGithubIssues", testGithubIssues), ("testHelpers", testHelpers), ("testResizeFile", testResizeFile), @@ -89,6 +89,15 @@ extension FilePermissionsTest { ] } +extension InternalUnitTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__InternalUnitTests = [ + ("testFileOffsets", testFileOffsets), + ] +} + extension MockingTest { // DO NOT MODIFY: This is autogenerated, use: // `swift test --generate-linuxmain` @@ -133,6 +142,16 @@ extension SystemStringTest { ] } +extension UtilTests { + // DO NOT MODIFY: This is autogenerated, use: + // `swift test --generate-linuxmain` + // to regenerate. + static let __allTests__UtilTests = [ + ("testCStringArray", testCStringArray), + ("testStackBuffer", testStackBuffer), + ] +} + public func __allTests() -> [XCTestCaseEntry] { return [ testCase(ErrnoTest.__allTests__ErrnoTest), @@ -143,9 +162,11 @@ public func __allTests() -> [XCTestCaseEntry] { testCase(FilePathSyntaxTest.__allTests__FilePathSyntaxTest), testCase(FilePathTest.__allTests__FilePathTest), testCase(FilePermissionsTest.__allTests__FilePermissionsTest), + testCase(InternalUnitTests.__allTests__InternalUnitTests), testCase(MockingTest.__allTests__MockingTest), testCase(SystemCharTest.__allTests__SystemCharTest), testCase(SystemStringTest.__allTests__SystemStringTest), + testCase(UtilTests.__allTests__UtilTests), ] } #endif From aea7ca42af032f0aa4f0d095e8f20ad23cbec986 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Wed, 8 Mar 2023 10:07:07 -0700 Subject: [PATCH 20/26] Fixup comments and names --- Sources/System/FileControl.swift | 141 ++++++- Sources/System/FileControlRaw.swift | 628 +++++++++++++++++++++++++++- Sources/System/FileLock.swift | 2 +- Sources/System/ProcessID.swift | 8 +- 4 files changed, 761 insertions(+), 18 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index eefb5f4b..e5d915b4 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -7,18 +7,143 @@ See https://swift.org/LICENSE.txt for license information */ +#if !os(Windows) + + // Strongly typed, Swifty interfaces to the most common and useful `fcntl` // commands. extension FileDescriptor { - internal func _fcntl( - _ cmd: Command, _ lock: inout FileDescriptor.FileLock, - retryOnInterrupt: Bool - ) -> Result<(), Errno> { - nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { - withUnsafeMutablePointer(to: &lock) { - system_fcntl(self.rawValue, cmd.rawValue, $0) - } + /// Get the flags associated with this file descriptor + /// + /// The corresponding C function is `fcntl` with the `F_GETFD` command. + @_alwaysEmitIntoClient + public func getFlags(retryOnInterrupt: Bool = true) throws -> Flags { + try Flags(rawValue: control( + .getFlags, retryOnInterrupt: retryOnInterrupt)) + } + + /// Set the file descriptor flags. + /// + /// The corresponding C function is `fcntl` with the `F_SETFD` command. + @_alwaysEmitIntoClient + public func setFlags( + _ value: Flags, retryOnInterrupt: Bool = true + ) throws { + _ = try control( + .setFlags, value.rawValue, retryOnInterrupt: retryOnInterrupt) + } + + /// Get descriptor status flags. + /// + /// The corresponding C function is `fcntl` with the `F_GETFL` command. + @_alwaysEmitIntoClient + public func getStatusFlags( + retryOnInterrupt: Bool = true + ) throws -> StatusFlags { + try StatusFlags(rawValue: control( + .getStatusFlags, retryOnInterrupt: retryOnInterrupt)) + } + + /// Set descriptor status flags. + /// + /// The corresponding C function is `fcntl` with the `F_SETFL` command. + @_alwaysEmitIntoClient + public func setStatusFlags( + _ flags: StatusFlags, retryOnInterrupt: Bool = true + ) throws { + _ = try control( + .setStatusFlags, flags.rawValue, retryOnInterrupt: retryOnInterrupt) + } +} + +extension FileDescriptor { + /// Get the process ID or process group currently receiv- + /// ing SIGIO and SIGURG signals. + /// + /// The corresponding C function is `fcntl` with the `F_GETOWN` command. + @_alwaysEmitIntoClient + public func getOwner( + retryOnInterrupt: Bool = true + ) throws -> (ProcessID, isGroup: Bool) { + let pidOrPGID = try control( + .getOwner, retryOnInterrupt: retryOnInterrupt) + if pidOrPGID < 0 { + return (ProcessID(rawValue: -pidOrPGID), isGroup: true) } + return (ProcessID(rawValue: pidOrPGID), isGroup: false) + } + + /// Set the process or process group to receive SIGIO and + /// SIGURG signals. + /// + /// The corresponding C function is `fcntl` with the `F_SETOWN` command. + @_alwaysEmitIntoClient + public func setOwner( + _ id: ProcessID, isGroup: Bool, retryOnInterrupt: Bool = true + ) throws { + let pidOrPGID = isGroup ? -id.rawValue : id.rawValue + _ = try control(.setOwner, pidOrPGID, retryOnInterrupt: retryOnInterrupt) } } + +extension FileDescriptor { + /// Duplicate this file descriptor and return the newly created copy. + /// + /// - Parameters: + /// - `minRawValue`: A lower bound on the new file descriptor's raw value. + /// - `closeOnExec`: Whether the new descriptor's `closeOnExec` flag is set. + /// - Returns: The lowest numbered available descriptor whose raw value is + /// greater than or equal to `minRawValue`. + /// + /// File descriptors are merely references to some underlying system resource. + /// The system does not distinguish between the original and the new file + /// descriptor in any way. For example, read, write and seek operations on + /// one of them also affect the logical file position in the other, and + /// append mode, non-blocking I/O and asynchronous I/O options are shared + /// between the references. If a separate pointer into the file is desired, + /// a different object reference to the file must be obtained by issuing an + /// additional call to `open`. + /// + /// However, each file descriptor maintains its own close-on-exec flag. + /// + /// The corresponding C functions are `fcntl` with `F_DUPFD` and + /// `F_DUPFD_CLOEXEC`. + @_alwaysEmitIntoClient + public func duplicate( + minRawValue: CInt, closeOnExec: Bool, retryOnInterrupt: Bool = true + ) throws -> FileDescriptor { + let cmd: Command = closeOnExec ? .duplicateCloseOnExec : .duplicate + return try FileDescriptor(rawValue: control( + cmd, minRawValue, retryOnInterrupt: retryOnInterrupt)) + } + + /// Get the path of the file descriptor + /// + /// - Parameters: + /// - `noFirmLink`: Get the non firmlinked path of the file descriptor. + /// + /// The corresponding C functions are `fcntl` with `F_GETPATH` and + /// `F_GETPATH_NOFIRMLINK`. + public func getPath( + noFirmLink: Bool = false, retryOnInterrupt: Bool = true + ) throws -> FilePath { + let cmd: Command = noFirmLink ? .getPathNoFirmLink : .getPath + // TODO: have a uninitialized init on FilePath / SystemString... + let bytes = try Array(unsafeUninitializedCapacity: _maxPathLen) { + (bufPtr, count: inout Int) in + _ = try control( + cmd, + UnsafeMutableRawPointer(bufPtr.baseAddress!), + retryOnInterrupt: retryOnInterrupt) + // TODO: The below is probably the wrong formulation... + count = system_strlen( + UnsafeRawPointer( + bufPtr.baseAddress! + ).assumingMemoryBound(to: Int8.self)) + } + return FilePath(SystemString(nullTerminated: bytes)) + } +} + +#endif // !os(Windows) diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 5a98e2a8..55fd56b9 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -20,44 +20,656 @@ import Glibc #if !os(Windows) +// MARK: - RawRepresentable wrappers +extension FileDescriptor { + /// File descriptor flags. + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + /// The given file descriptor will be automatically closed in the + /// successor process image when one of the execv(2) or posix_spawn(2) + /// family of system calls is invoked. + /// + /// The corresponding C global is `FD_CLOEXEC`. + @_alwaysEmitIntoClient + public static var closeOnExec: Flags { Flags(rawValue: FD_CLOEXEC) } + } + + /// File descriptor status flags. + @frozen + public struct StatusFlags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + fileprivate init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Non-blocking I/O; if no data is available to a read + /// call, or if a write operation would block, the read or + /// write call throws `Errno.resourceTemporarilyUnavailable`. + /// + /// The corresponding C constant is `O_NONBLOCK`. + @_alwaysEmitIntoClient + public static var nonBlocking: StatusFlags { StatusFlags(O_NONBLOCK) } + + /// Force each write to append at the end of file; corre- + /// sponds to `OpenOptions.append`. + /// + /// The corresponding C constant is `O_APPEND`. + @_alwaysEmitIntoClient + public static var append: StatusFlags { StatusFlags(O_APPEND) } + + /// Enable the SIGIO signal to be sent to the process + /// group when I/O is possible, e.g., upon availability of + /// data to be read. + /// + /// The corresponding C constant is `O_ASYNC`. + @_alwaysEmitIntoClient + public static var async: StatusFlags { StatusFlags(O_ASYNC) } + } +} + + // - MARK: Commands // TODO: Make below API as part of broader `fcntl` support. extension FileDescriptor { /// Commands (and various constants) to pass to `fcntl`. - internal struct Command: RawRepresentable, Hashable { - internal let rawValue: CInt + @frozen + public struct Command: RawRepresentable, Hashable, Codable, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInt - internal init(rawValue: CInt) { self.rawValue = rawValue } + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + @_alwaysEmitIntoClient private init(_ raw: CInt) { self.init(rawValue: raw) } + /// Duplicate file descriptor. + /// + /// Note: A Swiftier wrapper is + /// `FileDescriptor.duplicate(minRawValue:closeOnExec:)`. + /// + /// The corresponding C constant is `F_DUPFD`. + @_alwaysEmitIntoClient + public static var duplicate: Command { Command(F_DUPFD) } + + /// Get file descriptor flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getFlags()`. + /// + /// The corresponding C constant is `F_GETFD`. + @_alwaysEmitIntoClient + public static var getFlags: Command { Command(F_GETFD) } + + /// Set file descriptor flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setFlags(_:)`. + /// + /// The corresponding C constant is `F_SETFD`. + @_alwaysEmitIntoClient + public static var setFlags: Command { Command(F_SETFD) } + + /// Get file status flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getStatusFlags()`. + /// + /// The corresponding C constant is `F_GETFL`. + @_alwaysEmitIntoClient + public static var getStatusFlags: Command { + Command(F_GETFL) + } + + /// Set file status flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setStatusFlags(_:)`. + /// + /// The corresponding C constant is `F_SETFL`. + @_alwaysEmitIntoClient + public static var setStatusFlags: Command { + Command(F_SETFL) + } + + /// Get SIGIO/SIGURG proc/pgrp. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getOwner()`. + /// + /// The corresponding C constant is `F_GETOWN`. + @_alwaysEmitIntoClient + public static var getOwner: Command { Command(F_GETOWN) } + + /// Set SIGIO/SIGURG proc/pgrp. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setOwner(_:)`. + /// + /// The corresponding C constant is `F_SETOWN`. + @_alwaysEmitIntoClient + public static var setOwner: Command { Command(F_SETOWN) } + + /// Get record locking information. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getLock(_:_:)`. + /// + /// The corresponding C constant is `F_GETLK`. + @_alwaysEmitIntoClient + public static var getLock: Command { Command(F_GETLK) } + + /// Set record locking information. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. + /// + /// The corresponding C constant is `F_SETLK`. + @_alwaysEmitIntoClient + public static var setLock: Command { Command(F_SETLK) } + + /// Wait if blocked. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. + /// + /// The corresponding C constant is `F_SETLKW`. + @_alwaysEmitIntoClient + public static var setLockWait: Command { Command(F_SETLKW) } + + /// Wait if blocked, return on timeout. + /// + /// TODO: A Swiftier wrapper + /// + /// The corresponding C constant is `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public static var setLockWaitTimout: Command { + Command(F_SETLKWTIMEOUT) + } + + /// The corresponding C constant is `F_FLUSH_DATA`. + @_alwaysEmitIntoClient + public static var flushData: Command { + Command(F_FLUSH_DATA) + } + + /// Used for regression test. + /// + /// The corresponding C constant is `F_CHKCLEAN`. + @_alwaysEmitIntoClient + public static var checkClean: Command { Command(F_CHKCLEAN) } + + // TODO: Higher level API which will call `fallocate(2)` on Linux and use + // TODO: the below on Darwin. Then we can call out to that. + + /// Preallocate storage. + /// + /// The corresponding C constant is `F_PREALLOCATE`. + @_alwaysEmitIntoClient + public static var preallocate: Command { + Command(F_PREALLOCATE) + } + + /// Truncate a file. Equivalent to calling truncate(2). + /// + /// The corresponding C constant is `F_SETSIZE`. + @_alwaysEmitIntoClient + public static var setSize: Command { Command(F_SETSIZE) } + + /// Issue an advisory read async with no copy to user. + /// + /// The corresponding C constant is `F_RDADVISE`. + @_alwaysEmitIntoClient + public static var readAdvise: Command { Command(F_RDADVISE) } + + /// Turn read ahead off/on for this fd. + /// + /// The corresponding C constant is `F_RDAHEAD`. + @_alwaysEmitIntoClient + public static var readAhead: Command { Command(F_RDAHEAD) } + + /// Turn data caching off/on for this fd. + /// + /// The corresponding C constant is `F_NOCACHE`. + @_alwaysEmitIntoClient + public static var noCache: Command { Command(F_NOCACHE) } + + /// File offset to device offset. + /// + /// The corresponding C constant is `F_LOG2PHYS`. + @_alwaysEmitIntoClient + public static var logicalToPhysical: Command { Command(F_LOG2PHYS) } + + /// Return the full path of the fd. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. + /// + /// The corresponding C constant is `F_GETPATH`. + @_alwaysEmitIntoClient + public static var getPath: Command { Command(F_GETPATH) } + + /// Synchronize the file system and ask the drive to flush to the media. + /// + /// The corresponding C constant is `F_FULLFSYNC`. + @_alwaysEmitIntoClient + public static var fullFileSystemSync: Command { Command(F_FULLFSYNC) } + + /// Find which component (if any) is a package. + /// + /// The corresponding C constant is `F_PATHPKG_CHECK`. + @_alwaysEmitIntoClient + public static var pathPackageCheck: Command { + Command(F_PATHPKG_CHECK) + } + + /// "Freeze" all file system operations. + /// + /// The corresponding C constant is `F_FREEZE_FS`. + @_alwaysEmitIntoClient + public static var freezeFileSystem: Command { Command(F_FREEZE_FS) } + + /// "Thaw" all file system operations. + /// + /// The corresponding C constant is `F_THAW_FS`. + @_alwaysEmitIntoClient + public static var thawFileSystem: Command { Command(F_THAW_FS) } + + /// Turn data caching off/on (globally) for this file. + /// + /// The corresponding C constant is `F_GLOBAL_NOCACHE`. + @_alwaysEmitIntoClient + public static var globalNoCache: Command { + Command(F_GLOBAL_NOCACHE) + } + + /// Add detached signatures. + /// + /// The corresponding C constant is `F_ADDSIGS`. + @_alwaysEmitIntoClient + public static var addSignatures: Command { + Command(F_ADDSIGS) + } + + /// Add signature from same file (used by dyld for shared libs). + /// + /// The corresponding C constant is `F_ADDFILESIGS`. + @_alwaysEmitIntoClient + public static var addFileSignatures: Command { + Command(F_ADDFILESIGS) + } + + /// Used in conjunction with `.noCache` to indicate that DIRECT, + /// synchonous writes should not be used (i.e. its ok to temporaily create + /// cached pages). + /// + /// The corresponding C constant is `F_NODIRECT`. + @_alwaysEmitIntoClient + public static var noDirect: Command { Command(F_NODIRECT) } + + /// Get the protection class of a file from the EA, returns int. + /// + /// The corresponding C constant is `F_GETPROTECTIONCLASS`. + @_alwaysEmitIntoClient + public static var getProtectionClass: Command { + Command(F_GETPROTECTIONCLASS) + } + + /// Set the protection class of a file for the EA, requires int. + /// + /// The corresponding C constant is `F_SETPROTECTIONCLASS`. + @_alwaysEmitIntoClient + public static var setProtectionClass: Command { + Command(F_SETPROTECTIONCLASS) + } + + /// File offset to device offset, extended. + /// + /// The corresponding C constant is `F_LOG2PHYS_EXT`. + @_alwaysEmitIntoClient + public static var log2physExtended: Command { + Command(F_LOG2PHYS_EXT) + } + + /// Get record locking information, per-process. + /// + /// The corresponding C constant is `F_GETLKPID`. + @_alwaysEmitIntoClient + public static var getLockPID: Command { Command(F_GETLKPID) } + + /// Mark the dup with FD_CLOEXEC. + /// + /// The corresponding C constant is `F_DUPFD_CLOEXEC` + @_alwaysEmitIntoClient + public static var duplicateCloseOnExec: Command { + Command(F_DUPFD_CLOEXEC) + } + + /// Mark the file as being the backing store for another filesystem. + /// + /// The corresponding C constant is `F_SETBACKINGSTORE`. + @_alwaysEmitIntoClient + public static var setBackingStore: Command { + Command(F_SETBACKINGSTORE) + } + + /// Return the full path of the FD, but error in specific mtmd + /// circumstances. + /// + /// The corresponding C constant is `F_GETPATH_MTMINFO`. + @_alwaysEmitIntoClient + public static var getPathMTMDInfo: Command { + Command(F_GETPATH_MTMINFO) + } + + /// Returns the code directory, with associated hashes, to the caller. + /// + /// The corresponding C constant is `F_GETCODEDIR`. + @_alwaysEmitIntoClient + public static var getCodeDirectory: Command { + Command(F_GETCODEDIR) + } + + /// No SIGPIPE generated on EPIPE. + /// + /// The corresponding C constant is `F_SETNOSIGPIPE`. + @_alwaysEmitIntoClient + public static var setNoSigPipe: Command { + Command(F_SETNOSIGPIPE) + } + + /// Status of SIGPIPE for this fd. + /// + /// The corresponding C constant is `F_GETNOSIGPIPE`. + @_alwaysEmitIntoClient + public static var getNoSigPipe: Command { + Command(F_GETNOSIGPIPE) + } + + /// For some cases, we need to rewrap the key for AKS/MKB. + /// + /// The corresponding C constant is `F_TRANSCODEKEY`. + @_alwaysEmitIntoClient + public static var transcodeKey: Command { + Command(F_TRANSCODEKEY) + } + + /// File being written to a by single writer... if throttling enabled, + /// writes may be broken into smaller chunks with throttling in between. + /// + /// The corresponding C constant is `F_SINGLE_WRITER`. + @_alwaysEmitIntoClient + public static var singleWriter: Command { + Command(F_SINGLE_WRITER) + } + + + /// Get the protection version number for this filesystem. + /// + /// The corresponding C constant is `F_GETPROTECTIONLEVEL`. + @_alwaysEmitIntoClient + public static var getProtectionLevel: Command { + Command(F_GETPROTECTIONLEVEL) + } + + + /// Add detached code signatures (used by dyld for shared libs). + /// + /// The corresponding C constant is `F_FINDSIGS`. + @_alwaysEmitIntoClient + public static var findSignatures: Command { + Command(F_FINDSIGS) + } + + /// Add signature from same file, only if it is signed by Apple (used by + /// dyld for simulator). + /// + /// The corresponding C constant is `F_ADDFILESIGS_FOR_DYLD_SIM`. + @_alwaysEmitIntoClient + public static var addFileSignaturesForDYLDSim: Command { + Command(F_ADDFILESIGS_FOR_DYLD_SIM) + } + + /// Fsync + issue barrier to drive. + /// + /// The corresponding C constant is `F_BARRIERFSYNC`. + @_alwaysEmitIntoClient + public static var barrierFsync: Command { + Command(F_BARRIERFSYNC) + } + + /// Add signature from same file, return end offset in structure on success. + /// + /// The corresponding C constant is `F_ADDFILESIGS_RETURN`. + @_alwaysEmitIntoClient + public static var addFileSignaturesReturn: Command { + Command(F_ADDFILESIGS_RETURN) + } + + /// Check if Library Validation allows this Mach-O file to be mapped into + /// the calling process. + /// + /// The corresponding C constant is `F_CHECK_LV`. + @_alwaysEmitIntoClient + public static var checkLibraryValidation: Command { + Command(F_CHECK_LV) + } + + /// Deallocate a range of the file. + /// + /// The corresponding C constant is `F_PUNCHHOLE`. + @_alwaysEmitIntoClient + public static var punchhole: Command { Command(F_PUNCHHOLE) } + + /// Trim an active file. + /// + /// The corresponding C constant is `F_TRIM_ACTIVE_FILE`. + @_alwaysEmitIntoClient + public static var trimActiveFile: Command { + Command(F_TRIM_ACTIVE_FILE) + } + + /// Synchronous advisory read fcntl for regular and compressed file. + /// + /// The corresponding C constant is `F_SPECULATIVE_READ`. + @_alwaysEmitIntoClient + public static var speculativeRead: Command { + Command(F_SPECULATIVE_READ) + } + + /// Return the full path without firmlinks of the fd. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. + /// + /// The corresponding C constant is `F_GETPATH_NOFIRMLINK`. + @_alwaysEmitIntoClient + public static var getPathNoFirmLink: Command { + Command(F_GETPATH_NOFIRMLINK) + } + + /// Add signature from same file, return information. + /// + /// The corresponding C constant is `F_ADDFILESIGS_INFO`. + @_alwaysEmitIntoClient + public static var addFileSignatureInfo: Command { + Command(F_ADDFILESIGS_INFO) + } + + /// Add supplemental signature from same file with fd reference to original. + /// + /// The corresponding C constant is `F_ADDFILESUPPL`. + @_alwaysEmitIntoClient + public static var addFileSupplementalSignature: Command { + Command(F_ADDFILESUPPL) + } + + /// Look up code signature information attached to a file or slice. + /// + /// The corresponding C constant is `F_GETSIGSINFO`. + @_alwaysEmitIntoClient + public static var getSignatureInfo: Command { + Command(F_GETSIGSINFO) + } + + /// Allocate contigious space. + /// + /// TODO: This is actually a flag for the PREALLOCATE struct... + /// + /// The corresponding C constant is `F_ALLOCATECONTIG`. + @_alwaysEmitIntoClient + public static var allocateContiguous: Command { + Command(F_ALLOCATECONTIG) + } + + /// Allocate all requested space or no space at all. + /// + /// TODO: This is actually a flag for the PREALLOCATE struct... + /// + /// The corresponding C constant is `F_ALLOCATEALL`. + @_alwaysEmitIntoClient + public static var allocateAll: Command { + Command(F_ALLOCATEALL) + } + + /// Allocate from the physical end of file. In this case, fst_length + /// indicates the number of newly allocated bytes desired. + /// + /// TODO: This is actually a flag for the PREALLOCATE struct... + /// + /// The corresponding C constant is `F_PEOFPOSMODE`. + @_alwaysEmitIntoClient + public static var endOfFile: Command { + Command(F_PEOFPOSMODE) + } + + /// Specify volume starting postion. + /// + /// TODO: This is actually a flag for the PREALLOCATE struct... + /// + /// The corresponding C constant is `F_VOLPOSMODE`. + @_alwaysEmitIntoClient + public static var startOfVolume: Command { + Command(F_VOLPOSMODE) + } + /// Get open file description record locking information. /// /// The corresponding C constant is `F_GETLK`. - internal static var getOFDLock: Command { Command(_F_OFD_GETLK) } + @_alwaysEmitIntoClient + public static var getOFDLock: Command { Command(_F_OFD_GETLK) } /// Set open file description record locking information. /// /// The corresponding C constant is `F_SETLK`. - internal static var setOFDLock: Command { Command(_F_OFD_SETLK) } + @_alwaysEmitIntoClient + public static var setOFDLock: Command { Command(_F_OFD_SETLK) } /// Set open file description record locking information and wait until /// the request can be completed. /// /// The corresponding C constant is `F_SETLKW`. - internal static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } + @_alwaysEmitIntoClient + public static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } #if !os(Linux) /// Set open file description record locking information and wait until /// the request can be completed, returning on timeout. /// /// The corresponding C constant is `F_SETLKWTIMEOUT`. - internal static var setOFDLockWaitTimout: Command { + @_alwaysEmitIntoClient + public static var setOFDLockWaitTimout: Command { Command(_F_OFD_SETLKWTIMEOUT) } #endif } } -#endif + + +extension FileDescriptor { + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, arg, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ ptr: UnsafeMutableRawPointer, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, ptr, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ lock: inout FileDescriptor.FileLock, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, &lock, retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _control( + _ cmd: Command, retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue) + } + } + @usableFromInline + internal func _control( + _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue, arg) + } + } + @usableFromInline + internal func _control( + _ cmd: Command, + _ ptr: UnsafeMutableRawPointer, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue, ptr) + } + } + + @usableFromInline + internal func _control( + _ cmd: Command, + _ lock: inout FileDescriptor.FileLock, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + withUnsafeMutablePointer(to: &lock) { + system_fcntl(self.rawValue, cmd.rawValue, $0) + } + } + } +} + +internal var _maxPathLen: Int { Int(MAXPATHLEN) } + +#endif // !os(Windows) + diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 07093139..40b6e686 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -373,7 +373,7 @@ extension FileDescriptor { var lock = FileDescriptor.FileLock( ofdType: kind, start: start, length: length) return _extractWouldBlock( - _fcntl(cmd, &lock, retryOnInterrupt: retryOnInterrupt)) + _control(cmd, &lock, retryOnInterrupt: retryOnInterrupt)) } } diff --git a/Sources/System/ProcessID.swift b/Sources/System/ProcessID.swift index 70049a30..2f8ff9ab 100644 --- a/Sources/System/ProcessID.swift +++ b/Sources/System/ProcessID.swift @@ -10,7 +10,13 @@ public struct ProcessID: RawRepresentable, Hashable, Sendable { public var rawValue: CInterop.PID @_alwaysEmitIntoClient - public init(rawValue: CInterop.PID) { self.rawValue = rawValue } + public init(rawValue: CInterop.PID) { + // We assert instead of precondition, as the user may want to + // use this to ferry a PIDOrPGID value (denoted by a negative number). + // They would a `EINVAL` on use, instead of trapping the process. + assert(rawValue >= 0, "Process IDs are always positive") + self.rawValue = rawValue + } } #endif From 6bee7a21b4e6d429b40cd252c222e7974387548a Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Wed, 8 Mar 2023 10:21:01 -0700 Subject: [PATCH 21/26] Linux cleanup --- Sources/CSystem/include/CSystemLinux.h | 1 + Sources/System/FileControl.swift | 5 +++++ Sources/System/FileControlRaw.swift | 20 ++++++++++++-------- 3 files changed, 18 insertions(+), 8 deletions(-) diff --git a/Sources/CSystem/include/CSystemLinux.h b/Sources/CSystem/include/CSystemLinux.h index b172d658..70bd1474 100644 --- a/Sources/CSystem/include/CSystemLinux.h +++ b/Sources/CSystem/include/CSystemLinux.h @@ -11,6 +11,7 @@ #include #include +#include #include #include #include diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index e5d915b4..8b98d41e 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -118,6 +118,8 @@ extension FileDescriptor { cmd, minRawValue, retryOnInterrupt: retryOnInterrupt)) } +#if !os(Linux) + /// Get the path of the file descriptor /// /// - Parameters: @@ -144,6 +146,9 @@ extension FileDescriptor { } return FilePath(SystemString(nullTerminated: bytes)) } + +#endif // !os(Linux) + } #endif // !os(Windows) diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 55fd56b9..6849200a 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -103,6 +103,14 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var duplicate: Command { Command(F_DUPFD) } + /// Mark the dup with FD_CLOEXEC. + /// + /// The corresponding C constant is `F_DUPFD_CLOEXEC` + @_alwaysEmitIntoClient + public static var duplicateCloseOnExec: Command { + Command(F_DUPFD_CLOEXEC) + } + /// Get file descriptor flags. /// /// Note: A Swiftier wrapper is `FileDescriptor.getFlags()`. @@ -179,6 +187,8 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var setLockWait: Command { Command(F_SETLKW) } +#if !os(Linux) + /// Wait if blocked, return on timeout. /// /// TODO: A Swiftier wrapper @@ -338,14 +348,6 @@ extension FileDescriptor { @_alwaysEmitIntoClient public static var getLockPID: Command { Command(F_GETLKPID) } - /// Mark the dup with FD_CLOEXEC. - /// - /// The corresponding C constant is `F_DUPFD_CLOEXEC` - @_alwaysEmitIntoClient - public static var duplicateCloseOnExec: Command { - Command(F_DUPFD_CLOEXEC) - } - /// Mark the file as being the backing store for another filesystem. /// /// The corresponding C constant is `F_SETBACKINGSTORE`. @@ -553,6 +555,8 @@ extension FileDescriptor { Command(F_VOLPOSMODE) } +#endif // !os(Linux) + /// Get open file description record locking information. /// /// The corresponding C constant is `F_GETLK`. From 1880241dbb43e8995029b79ec3140a67ef6d45b9 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Wed, 8 Mar 2023 10:23:15 -0700 Subject: [PATCH 22/26] Fixups for Linux --- Sources/System/FileControlRaw.swift | 1 + 1 file changed, 1 insertion(+) diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 6849200a..9598877d 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -11,6 +11,7 @@ import Darwin #elseif os(Linux) || os(FreeBSD) || os(Android) import Glibc +import CSystem #elseif os(Windows) // Nothing #else From 547a1ff2496e54f1b3d1ec6d7f2ee602f95e11ba Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Sun, 12 Mar 2023 09:26:42 -0600 Subject: [PATCH 23/26] wip: add some more raw types, reorganize code somewhat --- Sources/System/FileControl.swift | 82 +++ Sources/System/FileControlCommand.swift | 495 +++++++++++++++++ Sources/System/FileControlRaw.swift | 680 ------------------------ Sources/System/FileControlTypes.swift | 163 ++++++ Sources/System/Internals/CInterop.swift | 8 +- 5 files changed, 747 insertions(+), 681 deletions(-) create mode 100644 Sources/System/FileControlCommand.swift create mode 100644 Sources/System/FileControlTypes.swift diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index 8b98d41e..347bc2f6 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -151,4 +151,86 @@ extension FileDescriptor { } +extension FileDescriptor { + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, arg, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ ptr: UnsafeMutableRawPointer, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, ptr, retryOnInterrupt: retryOnInterrupt).get() + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ lock: inout FileDescriptor.FileLock, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try _control(cmd, &lock, retryOnInterrupt: retryOnInterrupt).get() + } + + @usableFromInline + internal func _control( + _ cmd: Command, retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue) + } + } + @usableFromInline + internal func _control( + _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue, arg) + } + } + @usableFromInline + internal func _control( + _ cmd: Command, + _ ptr: UnsafeMutableRawPointer, + retryOnInterrupt: Bool + ) -> Result { + valueOrErrno(retryOnInterrupt: retryOnInterrupt) { + system_fcntl(self.rawValue, cmd.rawValue, ptr) + } + } + + @usableFromInline + internal func _control( + _ cmd: Command, + _ lock: inout FileDescriptor.FileLock, + retryOnInterrupt: Bool + ) -> Result<(), Errno> { + nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { + withUnsafeMutablePointer(to: &lock) { + system_fcntl(self.rawValue, cmd.rawValue, $0) + } + } + } +} + #endif // !os(Windows) diff --git a/Sources/System/FileControlCommand.swift b/Sources/System/FileControlCommand.swift new file mode 100644 index 00000000..55be9d21 --- /dev/null +++ b/Sources/System/FileControlCommand.swift @@ -0,0 +1,495 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +import CSystem +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + +#if !os(Windows) + +extension FileDescriptor { + /// Commands (and various constants) to pass to `fcntl`. + @frozen + public struct Command: RawRepresentable, Hashable, Codable, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + } +} + +extension FileDescriptor.Command { + @_alwaysEmitIntoClient + private init(_ rawValue: CInt) { self.init(rawValue: rawValue) } + + /// Duplicate file descriptor. + /// + /// Note: A Swiftier wrapper is + /// `FileDescriptor.duplicate(minRawValue:closeOnExec:)`. + /// + /// The corresponding C constant is `F_DUPFD`. + @_alwaysEmitIntoClient + public static var duplicate: Self { .init(F_DUPFD) } + + /// Mark the dup with FD_CLOEXEC. + /// + /// The corresponding C constant is `F_DUPFD_CLOEXEC` + @_alwaysEmitIntoClient + public static var duplicateCloseOnExec: Self { + .init(F_DUPFD_CLOEXEC) + } + + /// Get file descriptor flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getFlags()`. + /// + /// The corresponding C constant is `F_GETFD`. + @_alwaysEmitIntoClient + public static var getFlags: Self { .init(F_GETFD) } + + /// Set file descriptor flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setFlags(_:)`. + /// + /// The corresponding C constant is `F_SETFD`. + @_alwaysEmitIntoClient + public static var setFlags: Self { .init(F_SETFD) } + + /// Get file status flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getStatusFlags()`. + /// + /// The corresponding C constant is `F_GETFL`. + @_alwaysEmitIntoClient + public static var getStatusFlags: Self { + .init(F_GETFL) + } + + /// Set file status flags. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setStatusFlags(_:)`. + /// + /// The corresponding C constant is `F_SETFL`. + @_alwaysEmitIntoClient + public static var setStatusFlags: Self { + .init(F_SETFL) + } + + /// Get SIGIO/SIGURG proc/pgrp. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getOwner()`. + /// + /// The corresponding C constant is `F_GETOWN`. + @_alwaysEmitIntoClient + public static var getOwner: Self { .init(F_GETOWN) } + + /// Set SIGIO/SIGURG proc/pgrp. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setOwner(_:)`. + /// + /// The corresponding C constant is `F_SETOWN`. + @_alwaysEmitIntoClient + public static var setOwner: Self { .init(F_SETOWN) } + + /// Get record locking information. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getLock(_:_:)`. + /// + /// The corresponding C constant is `F_GETLK`. + @_alwaysEmitIntoClient + public static var getLock: Self { .init(F_GETLK) } + + /// Set record locking information. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. + /// + /// The corresponding C constant is `F_SETLK`. + @_alwaysEmitIntoClient + public static var setLock: Self { .init(F_SETLK) } + + /// Wait if blocked. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. + /// + /// The corresponding C constant is `F_SETLKW`. + @_alwaysEmitIntoClient + public static var setLockWait: Self { .init(F_SETLKW) } + +#if !os(Linux) + + /// Wait if blocked, return on timeout. + /// + /// TODO: A Swiftier wrapper + /// + /// The corresponding C constant is `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public static var setLockWaitTimout: Self { + .init(F_SETLKWTIMEOUT) + } + + /// The corresponding C constant is `F_FLUSH_DATA`. + @_alwaysEmitIntoClient + public static var flushData: Self { + .init(F_FLUSH_DATA) + } + + /// Used for regression test. + /// + /// The corresponding C constant is `F_CHKCLEAN`. + @_alwaysEmitIntoClient + public static var checkClean: Self { .init(F_CHKCLEAN) } + + // TODO: Higher level API which will call `fallocate(2)` on Linux and use + // TODO: the below on Darwin. Then we can call out to that. + + /// Preallocate storage. + /// + /// The corresponding C constant is `F_PREALLOCATE`. + @_alwaysEmitIntoClient + public static var preallocate: Self { + .init(F_PREALLOCATE) + } + + /// Truncate a file. Equivalent to calling truncate(2). + /// + /// The corresponding C constant is `F_SETSIZE`. + @_alwaysEmitIntoClient + public static var setSize: Self { .init(F_SETSIZE) } + + /// Issue an advisory read async with no copy to user. + /// + /// The corresponding C constant is `F_RDADVISE`. + @_alwaysEmitIntoClient + public static var readAdvise: Self { .init(F_RDADVISE) } + + /// Turn read ahead off/on for this fd. + /// + /// The corresponding C constant is `F_RDAHEAD`. + @_alwaysEmitIntoClient + public static var readAhead: Self { .init(F_RDAHEAD) } + + /// Turn data caching off/on for this fd. + /// + /// The corresponding C constant is `F_NOCACHE`. + @_alwaysEmitIntoClient + public static var noCache: Self { .init(F_NOCACHE) } + + /// File offset to device offset. + /// + /// The corresponding C constant is `F_LOG2PHYS`. + @_alwaysEmitIntoClient + public static var logicalToPhysical: Self { .init(F_LOG2PHYS) } + + /// Return the full path of the fd. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. + /// + /// The corresponding C constant is `F_GETPATH`. + @_alwaysEmitIntoClient + public static var getPath: Self { .init(F_GETPATH) } + + /// Synchronize the file system and ask the drive to flush to the media. + /// + /// The corresponding C constant is `F_FULLFSYNC`. + @_alwaysEmitIntoClient + public static var fullFileSystemSync: Self { .init(F_FULLFSYNC) } + + /// Find which component (if any) is a package. + /// + /// The corresponding C constant is `F_PATHPKG_CHECK`. + @_alwaysEmitIntoClient + public static var pathPackageCheck: Self { + .init(F_PATHPKG_CHECK) + } + + /// "Freeze" all file system operations. + /// + /// The corresponding C constant is `F_FREEZE_FS`. + @_alwaysEmitIntoClient + public static var freezeFileSystem: Self { .init(F_FREEZE_FS) } + + /// "Thaw" all file system operations. + /// + /// The corresponding C constant is `F_THAW_FS`. + @_alwaysEmitIntoClient + public static var thawFileSystem: Self { .init(F_THAW_FS) } + + /// Turn data caching off/on (globally) for this file. + /// + /// The corresponding C constant is `F_GLOBAL_NOCACHE`. + @_alwaysEmitIntoClient + public static var globalNoCache: Self { + .init(F_GLOBAL_NOCACHE) + } + + /// Add detached signatures. + /// + /// The corresponding C constant is `F_ADDSIGS`. + @_alwaysEmitIntoClient + public static var addSignatures: Self { + .init(F_ADDSIGS) + } + + /// Add signature from same file (used by dyld for shared libs). + /// + /// The corresponding C constant is `F_ADDFILESIGS`. + @_alwaysEmitIntoClient + public static var addFileSignatures: Self { + .init(F_ADDFILESIGS) + } + + /// Used in conjunction with `.noCache` to indicate that DIRECT, + /// synchonous writes should not be used (i.e. its ok to temporaily create + /// cached pages). + /// + /// The corresponding C constant is `F_NODIRECT`. + @_alwaysEmitIntoClient + public static var noDirect: Self { .init(F_NODIRECT) } + + /// Get the protection class of a file from the EA, returns int. + /// + /// The corresponding C constant is `F_GETPROTECTIONCLASS`. + @_alwaysEmitIntoClient + public static var getProtectionClass: Self { + .init(F_GETPROTECTIONCLASS) + } + + /// Set the protection class of a file for the EA, requires int. + /// + /// The corresponding C constant is `F_SETPROTECTIONCLASS`. + @_alwaysEmitIntoClient + public static var setProtectionClass: Self { + .init(F_SETPROTECTIONCLASS) + } + + /// File offset to device offset, extended. + /// + /// The corresponding C constant is `F_LOG2PHYS_EXT`. + @_alwaysEmitIntoClient + public static var log2physExtended: Self { + .init(F_LOG2PHYS_EXT) + } + + /// Get record locking information, per-process. + /// + /// The corresponding C constant is `F_GETLKPID`. + @_alwaysEmitIntoClient + public static var getLockPID: Self { .init(F_GETLKPID) } + + /// Mark the file as being the backing store for another filesystem. + /// + /// The corresponding C constant is `F_SETBACKINGSTORE`. + @_alwaysEmitIntoClient + public static var setBackingStore: Self { + .init(F_SETBACKINGSTORE) + } + + /// Return the full path of the FD, but error in specific mtmd + /// circumstances. + /// + /// The corresponding C constant is `F_GETPATH_MTMINFO`. + @_alwaysEmitIntoClient + public static var getPathMTMDInfo: Self { + .init(F_GETPATH_MTMINFO) + } + + /// Returns the code directory, with associated hashes, to the caller. + /// + /// The corresponding C constant is `F_GETCODEDIR`. + @_alwaysEmitIntoClient + public static var getCodeDirectory: Self { + .init(F_GETCODEDIR) + } + + /// No SIGPIPE generated on EPIPE. + /// + /// The corresponding C constant is `F_SETNOSIGPIPE`. + @_alwaysEmitIntoClient + public static var setNoSigPipe: Self { + .init(F_SETNOSIGPIPE) + } + + /// Status of SIGPIPE for this fd. + /// + /// The corresponding C constant is `F_GETNOSIGPIPE`. + @_alwaysEmitIntoClient + public static var getNoSigPipe: Self { + .init(F_GETNOSIGPIPE) + } + + /// For some cases, we need to rewrap the key for AKS/MKB. + /// + /// The corresponding C constant is `F_TRANSCODEKEY`. + @_alwaysEmitIntoClient + public static var transcodeKey: Self { + .init(F_TRANSCODEKEY) + } + + /// File being written to a by single writer... if throttling enabled, + /// writes may be broken into smaller chunks with throttling in between. + /// + /// The corresponding C constant is `F_SINGLE_WRITER`. + @_alwaysEmitIntoClient + public static var singleWriter: Self { + .init(F_SINGLE_WRITER) + } + + + /// Get the protection version number for this filesystem. + /// + /// The corresponding C constant is `F_GETPROTECTIONLEVEL`. + @_alwaysEmitIntoClient + public static var getProtectionLevel: Self { + .init(F_GETPROTECTIONLEVEL) + } + + + /// Add detached code signatures (used by dyld for shared libs). + /// + /// The corresponding C constant is `F_FINDSIGS`. + @_alwaysEmitIntoClient + public static var findSignatures: Self { + .init(F_FINDSIGS) + } + + /// Add signature from same file, only if it is signed by Apple (used by + /// dyld for simulator). + /// + /// The corresponding C constant is `F_ADDFILESIGS_FOR_DYLD_SIM`. + @_alwaysEmitIntoClient + public static var addFileSignaturesForDYLDSim: Self { + .init(F_ADDFILESIGS_FOR_DYLD_SIM) + } + + /// Fsync + issue barrier to drive. + /// + /// The corresponding C constant is `F_BARRIERFSYNC`. + @_alwaysEmitIntoClient + public static var barrierFileSystemSync: Self { + .init(F_BARRIERFSYNC) + } + + /// Add signature from same file, return end offset in structure on success. + /// + /// The corresponding C constant is `F_ADDFILESIGS_RETURN`. + @_alwaysEmitIntoClient + public static var addFileSignaturesReturn: Self { + .init(F_ADDFILESIGS_RETURN) + } + + /// Check if Library Validation allows this Mach-O file to be mapped into + /// the calling process. + /// + /// The corresponding C constant is `F_CHECK_LV`. + @_alwaysEmitIntoClient + public static var checkLibraryValidation: Self { + .init(F_CHECK_LV) + } + + /// Deallocate a range of the file. + /// + /// The corresponding C constant is `F_PUNCHHOLE`. + @_alwaysEmitIntoClient + public static var punchhole: Self { .init(F_PUNCHHOLE) } + + /// Trim an active file. + /// + /// The corresponding C constant is `F_TRIM_ACTIVE_FILE`. + @_alwaysEmitIntoClient + public static var trimActiveFile: Self { + .init(F_TRIM_ACTIVE_FILE) + } + + /// Synchronous advisory read fcntl for regular and compressed file. + /// + /// The corresponding C constant is `F_SPECULATIVE_READ`. + @_alwaysEmitIntoClient + public static var speculativeRead: Self { + .init(F_SPECULATIVE_READ) + } + + /// Return the full path without firmlinks of the fd. + /// + /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. + /// + /// The corresponding C constant is `F_GETPATH_NOFIRMLINK`. + @_alwaysEmitIntoClient + public static var getPathNoFirmLink: Self { + .init(F_GETPATH_NOFIRMLINK) + } + + /// Add signature from same file, return information. + /// + /// The corresponding C constant is `F_ADDFILESIGS_INFO`. + @_alwaysEmitIntoClient + public static var addFileSignatureInfo: Self { + .init(F_ADDFILESIGS_INFO) + } + + /// Add supplemental signature from same file with fd reference to original. + /// + /// The corresponding C constant is `F_ADDFILESUPPL`. + @_alwaysEmitIntoClient + public static var addFileSupplementalSignature: Self { + .init(F_ADDFILESUPPL) + } + + /// Look up code signature information attached to a file or slice. + /// + /// The corresponding C constant is `F_GETSIGSINFO`. + @_alwaysEmitIntoClient + public static var getSignatureInfo: Self { + .init(F_GETSIGSINFO) + } + +#endif // !os(Linux) + + /// Get open file description record locking information. + /// + /// The corresponding C constant is `F_GETLK`. + @_alwaysEmitIntoClient + public static var getOFDLock: Self { .init(_F_OFD_GETLK) } + + /// Set open file description record locking information. + /// + /// The corresponding C constant is `F_SETLK`. + @_alwaysEmitIntoClient + public static var setOFDLock: Self { .init(_F_OFD_SETLK) } + + /// Set open file description record locking information and wait until + /// the request can be completed. + /// + /// The corresponding C constant is `F_SETLKW`. + @_alwaysEmitIntoClient + public static var setOFDLockWait: Self { .init(_F_OFD_SETLKW) } + +#if !os(Linux) + /// Set open file description record locking information and wait until + /// the request can be completed, returning on timeout. + /// + /// The corresponding C constant is `F_SETLKWTIMEOUT`. + @_alwaysEmitIntoClient + public static var setOFDLockWaitTimout: Self { + .init(_F_OFD_SETLKWTIMEOUT) + } +#endif +} + +internal var _maxPathLen: Int { Int(MAXPATHLEN) } + +#endif // !os(Windows) + diff --git a/Sources/System/FileControlRaw.swift b/Sources/System/FileControlRaw.swift index 9598877d..e69de29b 100644 --- a/Sources/System/FileControlRaw.swift +++ b/Sources/System/FileControlRaw.swift @@ -1,680 +0,0 @@ -/* - This source file is part of the Swift System open source project - - Copyright (c) 2021 Apple Inc. and the Swift System project authors - Licensed under Apache License v2.0 with Runtime Library Exception - - See https://swift.org/LICENSE.txt for license information - */ - -#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) -import Darwin -#elseif os(Linux) || os(FreeBSD) || os(Android) -import Glibc -import CSystem -#elseif os(Windows) -// Nothing -#else -#error("Unsupported Platform") -#endif - - -#if !os(Windows) - -// MARK: - RawRepresentable wrappers -extension FileDescriptor { - /// File descriptor flags. - @frozen - public struct Flags: OptionSet, Sendable { - @_alwaysEmitIntoClient - public let rawValue: CInt - - @_alwaysEmitIntoClient - public init(rawValue: CInt) { self.rawValue = rawValue } - - /// The given file descriptor will be automatically closed in the - /// successor process image when one of the execv(2) or posix_spawn(2) - /// family of system calls is invoked. - /// - /// The corresponding C global is `FD_CLOEXEC`. - @_alwaysEmitIntoClient - public static var closeOnExec: Flags { Flags(rawValue: FD_CLOEXEC) } - } - - /// File descriptor status flags. - @frozen - public struct StatusFlags: OptionSet, Sendable { - @_alwaysEmitIntoClient - public let rawValue: CInt - - @_alwaysEmitIntoClient - public init(rawValue: CInt) { self.rawValue = rawValue } - - @_alwaysEmitIntoClient - fileprivate init(_ raw: CInt) { self.init(rawValue: raw) } - - /// Non-blocking I/O; if no data is available to a read - /// call, or if a write operation would block, the read or - /// write call throws `Errno.resourceTemporarilyUnavailable`. - /// - /// The corresponding C constant is `O_NONBLOCK`. - @_alwaysEmitIntoClient - public static var nonBlocking: StatusFlags { StatusFlags(O_NONBLOCK) } - - /// Force each write to append at the end of file; corre- - /// sponds to `OpenOptions.append`. - /// - /// The corresponding C constant is `O_APPEND`. - @_alwaysEmitIntoClient - public static var append: StatusFlags { StatusFlags(O_APPEND) } - - /// Enable the SIGIO signal to be sent to the process - /// group when I/O is possible, e.g., upon availability of - /// data to be read. - /// - /// The corresponding C constant is `O_ASYNC`. - @_alwaysEmitIntoClient - public static var async: StatusFlags { StatusFlags(O_ASYNC) } - } -} - - -// - MARK: Commands - -// TODO: Make below API as part of broader `fcntl` support. -extension FileDescriptor { - /// Commands (and various constants) to pass to `fcntl`. - @frozen - public struct Command: RawRepresentable, Hashable, Codable, Sendable { - @_alwaysEmitIntoClient - public let rawValue: CInt - - @_alwaysEmitIntoClient - public init(rawValue: CInt) { self.rawValue = rawValue } - - @_alwaysEmitIntoClient - private init(_ raw: CInt) { self.init(rawValue: raw) } - - /// Duplicate file descriptor. - /// - /// Note: A Swiftier wrapper is - /// `FileDescriptor.duplicate(minRawValue:closeOnExec:)`. - /// - /// The corresponding C constant is `F_DUPFD`. - @_alwaysEmitIntoClient - public static var duplicate: Command { Command(F_DUPFD) } - - /// Mark the dup with FD_CLOEXEC. - /// - /// The corresponding C constant is `F_DUPFD_CLOEXEC` - @_alwaysEmitIntoClient - public static var duplicateCloseOnExec: Command { - Command(F_DUPFD_CLOEXEC) - } - - /// Get file descriptor flags. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getFlags()`. - /// - /// The corresponding C constant is `F_GETFD`. - @_alwaysEmitIntoClient - public static var getFlags: Command { Command(F_GETFD) } - - /// Set file descriptor flags. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.setFlags(_:)`. - /// - /// The corresponding C constant is `F_SETFD`. - @_alwaysEmitIntoClient - public static var setFlags: Command { Command(F_SETFD) } - - /// Get file status flags. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getStatusFlags()`. - /// - /// The corresponding C constant is `F_GETFL`. - @_alwaysEmitIntoClient - public static var getStatusFlags: Command { - Command(F_GETFL) - } - - /// Set file status flags. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.setStatusFlags(_:)`. - /// - /// The corresponding C constant is `F_SETFL`. - @_alwaysEmitIntoClient - public static var setStatusFlags: Command { - Command(F_SETFL) - } - - /// Get SIGIO/SIGURG proc/pgrp. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getOwner()`. - /// - /// The corresponding C constant is `F_GETOWN`. - @_alwaysEmitIntoClient - public static var getOwner: Command { Command(F_GETOWN) } - - /// Set SIGIO/SIGURG proc/pgrp. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.setOwner(_:)`. - /// - /// The corresponding C constant is `F_SETOWN`. - @_alwaysEmitIntoClient - public static var setOwner: Command { Command(F_SETOWN) } - - /// Get record locking information. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getLock(_:_:)`. - /// - /// The corresponding C constant is `F_GETLK`. - @_alwaysEmitIntoClient - public static var getLock: Command { Command(F_GETLK) } - - /// Set record locking information. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. - /// - /// The corresponding C constant is `F_SETLK`. - @_alwaysEmitIntoClient - public static var setLock: Command { Command(F_SETLK) } - - /// Wait if blocked. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.setLock(_:_:)`. - /// - /// The corresponding C constant is `F_SETLKW`. - @_alwaysEmitIntoClient - public static var setLockWait: Command { Command(F_SETLKW) } - -#if !os(Linux) - - /// Wait if blocked, return on timeout. - /// - /// TODO: A Swiftier wrapper - /// - /// The corresponding C constant is `F_SETLKWTIMEOUT`. - @_alwaysEmitIntoClient - public static var setLockWaitTimout: Command { - Command(F_SETLKWTIMEOUT) - } - - /// The corresponding C constant is `F_FLUSH_DATA`. - @_alwaysEmitIntoClient - public static var flushData: Command { - Command(F_FLUSH_DATA) - } - - /// Used for regression test. - /// - /// The corresponding C constant is `F_CHKCLEAN`. - @_alwaysEmitIntoClient - public static var checkClean: Command { Command(F_CHKCLEAN) } - - // TODO: Higher level API which will call `fallocate(2)` on Linux and use - // TODO: the below on Darwin. Then we can call out to that. - - /// Preallocate storage. - /// - /// The corresponding C constant is `F_PREALLOCATE`. - @_alwaysEmitIntoClient - public static var preallocate: Command { - Command(F_PREALLOCATE) - } - - /// Truncate a file. Equivalent to calling truncate(2). - /// - /// The corresponding C constant is `F_SETSIZE`. - @_alwaysEmitIntoClient - public static var setSize: Command { Command(F_SETSIZE) } - - /// Issue an advisory read async with no copy to user. - /// - /// The corresponding C constant is `F_RDADVISE`. - @_alwaysEmitIntoClient - public static var readAdvise: Command { Command(F_RDADVISE) } - - /// Turn read ahead off/on for this fd. - /// - /// The corresponding C constant is `F_RDAHEAD`. - @_alwaysEmitIntoClient - public static var readAhead: Command { Command(F_RDAHEAD) } - - /// Turn data caching off/on for this fd. - /// - /// The corresponding C constant is `F_NOCACHE`. - @_alwaysEmitIntoClient - public static var noCache: Command { Command(F_NOCACHE) } - - /// File offset to device offset. - /// - /// The corresponding C constant is `F_LOG2PHYS`. - @_alwaysEmitIntoClient - public static var logicalToPhysical: Command { Command(F_LOG2PHYS) } - - /// Return the full path of the fd. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. - /// - /// The corresponding C constant is `F_GETPATH`. - @_alwaysEmitIntoClient - public static var getPath: Command { Command(F_GETPATH) } - - /// Synchronize the file system and ask the drive to flush to the media. - /// - /// The corresponding C constant is `F_FULLFSYNC`. - @_alwaysEmitIntoClient - public static var fullFileSystemSync: Command { Command(F_FULLFSYNC) } - - /// Find which component (if any) is a package. - /// - /// The corresponding C constant is `F_PATHPKG_CHECK`. - @_alwaysEmitIntoClient - public static var pathPackageCheck: Command { - Command(F_PATHPKG_CHECK) - } - - /// "Freeze" all file system operations. - /// - /// The corresponding C constant is `F_FREEZE_FS`. - @_alwaysEmitIntoClient - public static var freezeFileSystem: Command { Command(F_FREEZE_FS) } - - /// "Thaw" all file system operations. - /// - /// The corresponding C constant is `F_THAW_FS`. - @_alwaysEmitIntoClient - public static var thawFileSystem: Command { Command(F_THAW_FS) } - - /// Turn data caching off/on (globally) for this file. - /// - /// The corresponding C constant is `F_GLOBAL_NOCACHE`. - @_alwaysEmitIntoClient - public static var globalNoCache: Command { - Command(F_GLOBAL_NOCACHE) - } - - /// Add detached signatures. - /// - /// The corresponding C constant is `F_ADDSIGS`. - @_alwaysEmitIntoClient - public static var addSignatures: Command { - Command(F_ADDSIGS) - } - - /// Add signature from same file (used by dyld for shared libs). - /// - /// The corresponding C constant is `F_ADDFILESIGS`. - @_alwaysEmitIntoClient - public static var addFileSignatures: Command { - Command(F_ADDFILESIGS) - } - - /// Used in conjunction with `.noCache` to indicate that DIRECT, - /// synchonous writes should not be used (i.e. its ok to temporaily create - /// cached pages). - /// - /// The corresponding C constant is `F_NODIRECT`. - @_alwaysEmitIntoClient - public static var noDirect: Command { Command(F_NODIRECT) } - - /// Get the protection class of a file from the EA, returns int. - /// - /// The corresponding C constant is `F_GETPROTECTIONCLASS`. - @_alwaysEmitIntoClient - public static var getProtectionClass: Command { - Command(F_GETPROTECTIONCLASS) - } - - /// Set the protection class of a file for the EA, requires int. - /// - /// The corresponding C constant is `F_SETPROTECTIONCLASS`. - @_alwaysEmitIntoClient - public static var setProtectionClass: Command { - Command(F_SETPROTECTIONCLASS) - } - - /// File offset to device offset, extended. - /// - /// The corresponding C constant is `F_LOG2PHYS_EXT`. - @_alwaysEmitIntoClient - public static var log2physExtended: Command { - Command(F_LOG2PHYS_EXT) - } - - /// Get record locking information, per-process. - /// - /// The corresponding C constant is `F_GETLKPID`. - @_alwaysEmitIntoClient - public static var getLockPID: Command { Command(F_GETLKPID) } - - /// Mark the file as being the backing store for another filesystem. - /// - /// The corresponding C constant is `F_SETBACKINGSTORE`. - @_alwaysEmitIntoClient - public static var setBackingStore: Command { - Command(F_SETBACKINGSTORE) - } - - /// Return the full path of the FD, but error in specific mtmd - /// circumstances. - /// - /// The corresponding C constant is `F_GETPATH_MTMINFO`. - @_alwaysEmitIntoClient - public static var getPathMTMDInfo: Command { - Command(F_GETPATH_MTMINFO) - } - - /// Returns the code directory, with associated hashes, to the caller. - /// - /// The corresponding C constant is `F_GETCODEDIR`. - @_alwaysEmitIntoClient - public static var getCodeDirectory: Command { - Command(F_GETCODEDIR) - } - - /// No SIGPIPE generated on EPIPE. - /// - /// The corresponding C constant is `F_SETNOSIGPIPE`. - @_alwaysEmitIntoClient - public static var setNoSigPipe: Command { - Command(F_SETNOSIGPIPE) - } - - /// Status of SIGPIPE for this fd. - /// - /// The corresponding C constant is `F_GETNOSIGPIPE`. - @_alwaysEmitIntoClient - public static var getNoSigPipe: Command { - Command(F_GETNOSIGPIPE) - } - - /// For some cases, we need to rewrap the key for AKS/MKB. - /// - /// The corresponding C constant is `F_TRANSCODEKEY`. - @_alwaysEmitIntoClient - public static var transcodeKey: Command { - Command(F_TRANSCODEKEY) - } - - /// File being written to a by single writer... if throttling enabled, - /// writes may be broken into smaller chunks with throttling in between. - /// - /// The corresponding C constant is `F_SINGLE_WRITER`. - @_alwaysEmitIntoClient - public static var singleWriter: Command { - Command(F_SINGLE_WRITER) - } - - - /// Get the protection version number for this filesystem. - /// - /// The corresponding C constant is `F_GETPROTECTIONLEVEL`. - @_alwaysEmitIntoClient - public static var getProtectionLevel: Command { - Command(F_GETPROTECTIONLEVEL) - } - - - /// Add detached code signatures (used by dyld for shared libs). - /// - /// The corresponding C constant is `F_FINDSIGS`. - @_alwaysEmitIntoClient - public static var findSignatures: Command { - Command(F_FINDSIGS) - } - - /// Add signature from same file, only if it is signed by Apple (used by - /// dyld for simulator). - /// - /// The corresponding C constant is `F_ADDFILESIGS_FOR_DYLD_SIM`. - @_alwaysEmitIntoClient - public static var addFileSignaturesForDYLDSim: Command { - Command(F_ADDFILESIGS_FOR_DYLD_SIM) - } - - /// Fsync + issue barrier to drive. - /// - /// The corresponding C constant is `F_BARRIERFSYNC`. - @_alwaysEmitIntoClient - public static var barrierFsync: Command { - Command(F_BARRIERFSYNC) - } - - /// Add signature from same file, return end offset in structure on success. - /// - /// The corresponding C constant is `F_ADDFILESIGS_RETURN`. - @_alwaysEmitIntoClient - public static var addFileSignaturesReturn: Command { - Command(F_ADDFILESIGS_RETURN) - } - - /// Check if Library Validation allows this Mach-O file to be mapped into - /// the calling process. - /// - /// The corresponding C constant is `F_CHECK_LV`. - @_alwaysEmitIntoClient - public static var checkLibraryValidation: Command { - Command(F_CHECK_LV) - } - - /// Deallocate a range of the file. - /// - /// The corresponding C constant is `F_PUNCHHOLE`. - @_alwaysEmitIntoClient - public static var punchhole: Command { Command(F_PUNCHHOLE) } - - /// Trim an active file. - /// - /// The corresponding C constant is `F_TRIM_ACTIVE_FILE`. - @_alwaysEmitIntoClient - public static var trimActiveFile: Command { - Command(F_TRIM_ACTIVE_FILE) - } - - /// Synchronous advisory read fcntl for regular and compressed file. - /// - /// The corresponding C constant is `F_SPECULATIVE_READ`. - @_alwaysEmitIntoClient - public static var speculativeRead: Command { - Command(F_SPECULATIVE_READ) - } - - /// Return the full path without firmlinks of the fd. - /// - /// Note: A Swiftier wrapper is `FileDescriptor.getPath(_:)`. - /// - /// The corresponding C constant is `F_GETPATH_NOFIRMLINK`. - @_alwaysEmitIntoClient - public static var getPathNoFirmLink: Command { - Command(F_GETPATH_NOFIRMLINK) - } - - /// Add signature from same file, return information. - /// - /// The corresponding C constant is `F_ADDFILESIGS_INFO`. - @_alwaysEmitIntoClient - public static var addFileSignatureInfo: Command { - Command(F_ADDFILESIGS_INFO) - } - - /// Add supplemental signature from same file with fd reference to original. - /// - /// The corresponding C constant is `F_ADDFILESUPPL`. - @_alwaysEmitIntoClient - public static var addFileSupplementalSignature: Command { - Command(F_ADDFILESUPPL) - } - - /// Look up code signature information attached to a file or slice. - /// - /// The corresponding C constant is `F_GETSIGSINFO`. - @_alwaysEmitIntoClient - public static var getSignatureInfo: Command { - Command(F_GETSIGSINFO) - } - - /// Allocate contigious space. - /// - /// TODO: This is actually a flag for the PREALLOCATE struct... - /// - /// The corresponding C constant is `F_ALLOCATECONTIG`. - @_alwaysEmitIntoClient - public static var allocateContiguous: Command { - Command(F_ALLOCATECONTIG) - } - - /// Allocate all requested space or no space at all. - /// - /// TODO: This is actually a flag for the PREALLOCATE struct... - /// - /// The corresponding C constant is `F_ALLOCATEALL`. - @_alwaysEmitIntoClient - public static var allocateAll: Command { - Command(F_ALLOCATEALL) - } - - /// Allocate from the physical end of file. In this case, fst_length - /// indicates the number of newly allocated bytes desired. - /// - /// TODO: This is actually a flag for the PREALLOCATE struct... - /// - /// The corresponding C constant is `F_PEOFPOSMODE`. - @_alwaysEmitIntoClient - public static var endOfFile: Command { - Command(F_PEOFPOSMODE) - } - - /// Specify volume starting postion. - /// - /// TODO: This is actually a flag for the PREALLOCATE struct... - /// - /// The corresponding C constant is `F_VOLPOSMODE`. - @_alwaysEmitIntoClient - public static var startOfVolume: Command { - Command(F_VOLPOSMODE) - } - -#endif // !os(Linux) - - /// Get open file description record locking information. - /// - /// The corresponding C constant is `F_GETLK`. - @_alwaysEmitIntoClient - public static var getOFDLock: Command { Command(_F_OFD_GETLK) } - - /// Set open file description record locking information. - /// - /// The corresponding C constant is `F_SETLK`. - @_alwaysEmitIntoClient - public static var setOFDLock: Command { Command(_F_OFD_SETLK) } - - /// Set open file description record locking information and wait until - /// the request can be completed. - /// - /// The corresponding C constant is `F_SETLKW`. - @_alwaysEmitIntoClient - public static var setOFDLockWait: Command { Command(_F_OFD_SETLKW) } - -#if !os(Linux) - /// Set open file description record locking information and wait until - /// the request can be completed, returning on timeout. - /// - /// The corresponding C constant is `F_SETLKWTIMEOUT`. - @_alwaysEmitIntoClient - public static var setOFDLockWaitTimout: Command { - Command(_F_OFD_SETLKWTIMEOUT) - } -#endif - - } -} - - -extension FileDescriptor { - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. - @_alwaysEmitIntoClient - public func control( - _ cmd: Command, retryOnInterrupt: Bool = true - ) throws -> CInt { - try _control(cmd, retryOnInterrupt: retryOnInterrupt).get() - } - - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. - @_alwaysEmitIntoClient - public func control( - _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool = true - ) throws -> CInt { - try _control(cmd, arg, retryOnInterrupt: retryOnInterrupt).get() - } - - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. - @_alwaysEmitIntoClient - public func control( - _ cmd: Command, - _ ptr: UnsafeMutableRawPointer, - retryOnInterrupt: Bool = true - ) throws -> CInt { - try _control(cmd, ptr, retryOnInterrupt: retryOnInterrupt).get() - } - - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. - @_alwaysEmitIntoClient - public func control( - _ cmd: Command, - _ lock: inout FileDescriptor.FileLock, - retryOnInterrupt: Bool = true - ) throws -> CInt { - try _control(cmd, &lock, retryOnInterrupt: retryOnInterrupt).get() - } - - @usableFromInline - internal func _control( - _ cmd: Command, retryOnInterrupt: Bool - ) -> Result { - valueOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_fcntl(self.rawValue, cmd.rawValue) - } - } - @usableFromInline - internal func _control( - _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool - ) -> Result { - valueOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_fcntl(self.rawValue, cmd.rawValue, arg) - } - } - @usableFromInline - internal func _control( - _ cmd: Command, - _ ptr: UnsafeMutableRawPointer, - retryOnInterrupt: Bool - ) -> Result { - valueOrErrno(retryOnInterrupt: retryOnInterrupt) { - system_fcntl(self.rawValue, cmd.rawValue, ptr) - } - } - - @usableFromInline - internal func _control( - _ cmd: Command, - _ lock: inout FileDescriptor.FileLock, - retryOnInterrupt: Bool - ) -> Result<(), Errno> { - nothingOrErrno(retryOnInterrupt: retryOnInterrupt) { - withUnsafeMutablePointer(to: &lock) { - system_fcntl(self.rawValue, cmd.rawValue, $0) - } - } - } -} - -internal var _maxPathLen: Int { Int(MAXPATHLEN) } - -#endif // !os(Windows) - diff --git a/Sources/System/FileControlTypes.swift b/Sources/System/FileControlTypes.swift new file mode 100644 index 00000000..6b7a39b8 --- /dev/null +++ b/Sources/System/FileControlTypes.swift @@ -0,0 +1,163 @@ +/* + This source file is part of the Swift System open source project + + Copyright (c) 2021 Apple Inc. and the Swift System project authors + Licensed under Apache License v2.0 with Runtime Library Exception + + See https://swift.org/LICENSE.txt for license information + */ + +#if os(macOS) || os(iOS) || os(watchOS) || os(tvOS) +import Darwin +#elseif os(Linux) || os(FreeBSD) || os(Android) +import Glibc +import CSystem +#elseif os(Windows) +// Nothing +#else +#error("Unsupported Platform") +#endif + +#if !os(Windows) + +// MARK: - RawRepresentable wrappers +extension FileDescriptor { + /// File descriptor flags. + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + /// The given file descriptor will be automatically closed in the + /// successor process image when one of the execv(2) or posix_spawn(2) + /// family of system calls is invoked. + /// + /// The corresponding C global is `FD_CLOEXEC`. + @_alwaysEmitIntoClient + public static var closeOnExec: Flags { Flags(rawValue: FD_CLOEXEC) } + } + + /// File descriptor status flags. + @frozen + public struct StatusFlags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + + @_alwaysEmitIntoClient + fileprivate init(_ raw: CInt) { self.init(rawValue: raw) } + + /// Non-blocking I/O; if no data is available to a read + /// call, or if a write operation would block, the read or + /// write call throws `Errno.resourceTemporarilyUnavailable`. + /// + /// The corresponding C constant is `O_NONBLOCK`. + @_alwaysEmitIntoClient + public static var nonBlocking: StatusFlags { StatusFlags(O_NONBLOCK) } + + /// Force each write to append at the end of file; corre- + /// sponds to `OpenOptions.append`. + /// + /// The corresponding C constant is `O_APPEND`. + @_alwaysEmitIntoClient + public static var append: StatusFlags { StatusFlags(O_APPEND) } + + /// Enable the SIGIO signal to be sent to the process + /// group when I/O is possible, e.g., upon availability of + /// data to be read. + /// + /// The corresponding C constant is `O_ASYNC`. + @_alwaysEmitIntoClient + public static var async: StatusFlags { StatusFlags(O_ASYNC) } + } +} + +#if !os(Linux) +extension FileDescriptor { + // TODO: Flatten these out? Rename this somehow? + @frozen + public enum ControlTypes { + /// TODO: preallocate description + @frozen + public struct Store: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInterop.FStore + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FStore) { self.rawValue = rawValue } + + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public let rawValue: UInt32 + + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } + } + + @frozen + public struct PositionMode: RawRepresentable, Hashable, Sendable { + @_alwaysEmitIntoClient + public let rawValue: CInt + + @_alwaysEmitIntoClient + public init(rawValue: CInt) { self.rawValue = rawValue } + } + } + } +} + +extension FileDescriptor.ControlTypes.Store.Flags { + @_alwaysEmitIntoClient + private init(_ rawSignedValue: Int32) { + self.init(rawValue: UInt32(truncatingIfNeeded: rawSignedValue)) + } + + /// Allocate contiguous space. (Note that the file system may + /// ignore this request if `length` is very large.) + /// + /// The corresponding C constant is `F_ALLOCATECONTIG` + @_alwaysEmitIntoClient + public var allocateContiguous: Self { .init(F_ALLOCATECONTIG) } + + /// Allocate all requested space or no space at all. + /// + /// The corresponding C constant is `F_ALLOCATEALL` + @_alwaysEmitIntoClient + public var allocateAllOrNone: Self { .init(F_ALLOCATEALL) } + + /// Allocate space that is not freed when close(2) is called. + /// (Note that the file system may ignore this request.) + /// + /// The corresponding C constant is `F_ALLOCATEPERSIST` + @_alwaysEmitIntoClient + public var allocatePersist: Self { .init(F_ALLOCATEPERSIST) } +} + +extension FileDescriptor.ControlTypes.Store.PositionMode { + /// Allocate from the physical end of file. In this case, `length` + /// indicates the number of newly allocated bytes desired. + /// + /// The corresponding C constant is `F_PEOFPOSMODE` + @_alwaysEmitIntoClient + public var physicalEndOfFile: Self { .init(rawValue: F_PEOFPOSMODE) } + + /// Allocate from the volume offset. + /// + /// The corresponding C constant is `F_VOLPOSMODE` + @_alwaysEmitIntoClient + public var volumeOffset: Self { .init(rawValue: F_VOLPOSMODE) } +} + +extension FileDescriptor.ControlTypes.Store { + +} + +#endif // !os(Linux) + +#endif // !os(Windows) diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index d8428b4f..7f6b5399 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -87,7 +87,13 @@ public enum CInterop { /// might otherwise appear. This typealias allows conversion code to be /// emitted into client. public typealias Offset = off_t -#endif + + #if !os(Linux) + /// The C `fstore` type + public typealias FStore = fstore + #endif + +#endif // !os(Windows) } From 979cb847195baf3ee9f3e1e2ef99368cc9a7303f Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Sun, 12 Mar 2023 10:03:52 -0600 Subject: [PATCH 24/26] Update comments --- Sources/System/FileLock.swift | 28 +++++++++++++++++----------- 1 file changed, 17 insertions(+), 11 deletions(-) diff --git a/Sources/System/FileLock.swift b/Sources/System/FileLock.swift index 40b6e686..9d9241bf 100644 --- a/Sources/System/FileLock.swift +++ b/Sources/System/FileLock.swift @@ -141,10 +141,12 @@ extension FileDescriptor.FileLock { extension FileDescriptor { /// Set an advisory open file description lock. /// - /// If the open file description already has a lock, the old lock is - /// replaced. If the lock cannot be set because it is blocked by an existing lock, - /// that is if the syscall would throw `.resourceTemporarilyUnavailable` - /// (aka `EAGAIN`), this will return `false`. + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock, that is if the syscall would throw + /// `.resourceTemporarilyUnavailable`(aka `EAGAIN`), this will return + /// `false`. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -191,9 +193,11 @@ extension FileDescriptor { /// Set an advisory open file description lock. /// - /// If the open file description already has a lock, the old lock is - /// replaced. If the lock cannot be set because it is blocked by an existing lock and - /// `wait` is true, this will wait until the lock can be set, otherwise returns `false`. + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock and `wait` is true, this will wait until + /// the lock can be set, otherwise returns `false`. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated @@ -243,10 +247,12 @@ extension FileDescriptor { #if !os(Linux) /// Set an advisory open file description lock. /// - /// If the open file description already has a lock, the old lock is - /// replaced. If the lock cannot be set because it is blocked by an existing lock and - /// `waitUntilTimeout` is true, this will wait until the lock can be set (or the operating - /// system's timeout expires), otherwise returns `false`. + /// If the open file description already has a lock over `byteRange`, that + /// portion of the old lock is replaced. If `byteRange` is `nil`, the + /// entire file is considered. If the lock cannot be set because it is + /// blocked by an existing lock and `waitUntilTimeout` is true, this will + /// wait until the lock can be set(or the operating system's timeout + /// expires), otherwise returns `false`. /// /// Open file description locks are associated with an open file /// description (see `FileDescriptor.open`). Duplicated From d3bcf7382c5df0ed495fddbee24cedb0388b1425 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Sun, 12 Mar 2023 10:55:28 -0600 Subject: [PATCH 25/26] Add in raw structs to finish fcntl for Darwin --- Sources/System/FileControl.swift | 60 +++++++- Sources/System/FileControlCommand.swift | 2 +- Sources/System/FileControlTypes.swift | 183 +++++++++++++++++++++++- Sources/System/Internals/CInterop.swift | 9 ++ 4 files changed, 246 insertions(+), 8 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index 347bc2f6..99f34783 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -192,6 +192,64 @@ extension FileDescriptor { try _control(cmd, &lock, retryOnInterrupt: retryOnInterrupt).get() } +#if !os(Linux) + + // TODO: Worth calling out the command to supply in docs? + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ type: inout FileDescriptor.ControlTypes.Store, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try withUnsafeMutablePointer(to: &type) { + try _control(cmd, $0, retryOnInterrupt: retryOnInterrupt).get() + } + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ type: inout FileDescriptor.ControlTypes.Punchhole, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try withUnsafeMutablePointer(to: &type) { + try _control(cmd, $0, retryOnInterrupt: retryOnInterrupt).get() + } + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ type: inout FileDescriptor.ControlTypes.ReadAdvisory, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try withUnsafeMutablePointer(to: &type) { + try _control(cmd, $0, retryOnInterrupt: retryOnInterrupt).get() + } + } + + /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier + /// alternatives directly on `FileDescriptor`. + @_alwaysEmitIntoClient + public func control( + _ cmd: Command, + _ type: inout FileDescriptor.ControlTypes.LogicalToPhysical, + retryOnInterrupt: Bool = true + ) throws -> CInt { + try withUnsafeMutablePointer(to: &type) { + try _control(cmd, $0, retryOnInterrupt: retryOnInterrupt).get() + } + } + +#endif // !os(Linux) + @usableFromInline internal func _control( _ cmd: Command, retryOnInterrupt: Bool @@ -218,7 +276,7 @@ extension FileDescriptor { system_fcntl(self.rawValue, cmd.rawValue, ptr) } } - + @usableFromInline internal func _control( _ cmd: Command, diff --git a/Sources/System/FileControlCommand.swift b/Sources/System/FileControlCommand.swift index 55be9d21..71183baf 100644 --- a/Sources/System/FileControlCommand.swift +++ b/Sources/System/FileControlCommand.swift @@ -280,7 +280,7 @@ extension FileDescriptor.Command { /// /// The corresponding C constant is `F_LOG2PHYS_EXT`. @_alwaysEmitIntoClient - public static var log2physExtended: Self { + public static var logicalToPhysicalExtended: Self { .init(F_LOG2PHYS_EXT) } diff --git a/Sources/System/FileControlTypes.swift b/Sources/System/FileControlTypes.swift index 6b7a39b8..29cd59f0 100644 --- a/Sources/System/FileControlTypes.swift +++ b/Sources/System/FileControlTypes.swift @@ -26,7 +26,7 @@ extension FileDescriptor { @frozen public struct Flags: OptionSet, Sendable { @_alwaysEmitIntoClient - public let rawValue: CInt + public var rawValue: CInt @_alwaysEmitIntoClient public init(rawValue: CInt) { self.rawValue = rawValue } @@ -44,7 +44,7 @@ extension FileDescriptor { @frozen public struct StatusFlags: OptionSet, Sendable { @_alwaysEmitIntoClient - public let rawValue: CInt + public var rawValue: CInt @_alwaysEmitIntoClient public init(rawValue: CInt) { self.rawValue = rawValue } @@ -82,11 +82,11 @@ extension FileDescriptor { // TODO: Flatten these out? Rename this somehow? @frozen public enum ControlTypes { - /// TODO: preallocate description + /// The corresponding C type is `fstore`. @frozen public struct Store: RawRepresentable, Sendable { @_alwaysEmitIntoClient - public let rawValue: CInterop.FStore + public var rawValue: CInterop.FStore @_alwaysEmitIntoClient public init(rawValue: CInterop.FStore) { self.rawValue = rawValue } @@ -94,7 +94,7 @@ extension FileDescriptor { @frozen public struct Flags: OptionSet, Sendable { @_alwaysEmitIntoClient - public let rawValue: UInt32 + public var rawValue: UInt32 @_alwaysEmitIntoClient public init(rawValue: UInt32) { self.rawValue = rawValue } @@ -103,12 +103,60 @@ extension FileDescriptor { @frozen public struct PositionMode: RawRepresentable, Hashable, Sendable { @_alwaysEmitIntoClient - public let rawValue: CInt + public var rawValue: CInt @_alwaysEmitIntoClient public init(rawValue: CInt) { self.rawValue = rawValue } } } + + /// The corresponding C type is `fpunchhole` + @frozen + public struct Punchhole: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FPunchhole + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FPunchhole) { self.rawValue = rawValue } + + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public var rawValue: UInt32 + + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } + } + } + + /// The corresponding C type is `radvisory` + @frozen + public struct ReadAdvisory: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.RAdvisory + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.RAdvisory) { self.rawValue = rawValue } + } + + /// The corresponding C type is `log2phys` + @frozen + public struct LogicalToPhysical: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.Log2Phys + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Log2Phys) { self.rawValue = rawValue } + + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public let rawValue: UInt32 + + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } + } + } } } @@ -155,7 +203,130 @@ extension FileDescriptor.ControlTypes.Store.PositionMode { } extension FileDescriptor.ControlTypes.Store { + /// The corresponding C field is `fst_flags` + @_alwaysEmitIntoClient + public var flags: Flags { + get { .init(rawValue: rawValue.fst_flags) } + set { rawValue.fst_flags = newValue.rawValue } + } + + /// Indicates mode for offset field + /// + /// The corresponding C field is `fst_posmode` + @_alwaysEmitIntoClient + public var positionMode: PositionMode { + get { .init(rawValue: rawValue.fst_posmode) } + set { rawValue.fst_posmode = newValue.rawValue } + } + + /// Start of the region + /// + /// The corresponding C field is `fst_offset` + @_alwaysEmitIntoClient + public var offset: Int64 { + get { .init(rawValue.fst_offset) } + set { rawValue.fst_offset = CInterop.Offset(newValue) } + } + + /// Size of the region + /// + /// The corresponding C field is `fst_length` + @_alwaysEmitIntoClient + public var length: Int64 { + get { .init(rawValue.fst_length) } + set { rawValue.fst_length = CInterop.Offset(newValue) } + } + + /// Output: number of bytes allocated + /// + /// The corresponding C field is `fst_bytesalloc` + @_alwaysEmitIntoClient + public var bytesAllocated: Int64 { + get { .init(rawValue.fst_bytesalloc) } + set { rawValue.fst_bytesalloc = CInterop.Offset(newValue) } + } +} + +extension FileDescriptor.ControlTypes.Punchhole { + /// The corresponding C field is `fp_flags` + @_alwaysEmitIntoClient + public var flags: Flags { + get { .init(rawValue: rawValue.fp_flags) } + set { rawValue.fp_flags = newValue.rawValue } + } + + // No API for the reserved field + + /// Start of the region + /// + /// The corresponding C field is `fp_offset` + @_alwaysEmitIntoClient + public var offset: Int64 { + get { .init(rawValue.fp_offset) } + set { rawValue.fp_offset = CInterop.Offset(newValue) } + } + + /// Size of the region + /// + /// The corresponding C field is `fp_length` + @_alwaysEmitIntoClient + public var length: Int64 { + get { .init(rawValue.fp_length) } + set { rawValue.fp_length = CInterop.Offset(newValue) } + } +} + +extension FileDescriptor.ControlTypes.ReadAdvisory { + /// Offset into the file + /// + /// The corresponding C field is `ra_offset` + @_alwaysEmitIntoClient + public var offset: Int64 { + get { .init(rawValue.ra_offset) } + set { rawValue.ra_offset = CInterop.Offset(newValue) } + } + + /// Size of the read + /// + /// The corresponding C field is `ra_count` + @_alwaysEmitIntoClient + public var count: Int { + get { .init(rawValue.ra_count) } + set { rawValue.ra_count = CInt(newValue) } + } +} +extension FileDescriptor.ControlTypes.LogicalToPhysical { + /// The corresponding C field is `l2p_flags` + @_alwaysEmitIntoClient + public var flags: Flags { + get { .init(rawValue: rawValue.l2p_flags) } + set { rawValue.l2p_flags = newValue.rawValue } + } + + /// When used with `logicalToPhysicalExtended`: + /// - In: number of bytes to be queried; + /// - Out: number of contiguous bytes allocated at this position */ + /// + /// The corresponding C field is `l2p_contigbytes` + @_alwaysEmitIntoClient + public var contiguousBytes: Int64 { + get { .init(rawValue.l2p_contigbytes) } + set { rawValue.l2p_contigbytes = CInterop.Offset(newValue) } + } + + /// When used with `logicalToPhysical`, bytes into file. + /// + /// When used with `logicalToPhysicalExtended`: + /// - In: bytes into file + /// - Out: bytes into device + /// + /// The corresponding C field is `l2p_devoffset` + @_alwaysEmitIntoClient + public var deviceOffset: Int64 { + get { .init(rawValue.l2p_devoffset) } + set { rawValue.l2p_devoffset = CInterop.Offset(newValue) } + } } #endif // !os(Linux) diff --git a/Sources/System/Internals/CInterop.swift b/Sources/System/Internals/CInterop.swift index 7f6b5399..7fb416dd 100644 --- a/Sources/System/Internals/CInterop.swift +++ b/Sources/System/Internals/CInterop.swift @@ -91,6 +91,15 @@ public enum CInterop { #if !os(Linux) /// The C `fstore` type public typealias FStore = fstore + + /// The C `fpunchhole` type + public typealias FPunchhole = fpunchhole + + /// The C `radvisory` type + public typealias RAdvisory = radvisory + + /// The C `radvisory` type + public typealias Log2Phys = log2phys #endif #endif // !os(Windows) From dc4270d856bfa89a5e31ddc0a3af54e83772cfc8 Mon Sep 17 00:00:00 2001 From: Michael Ilseman Date: Thu, 16 Mar 2023 09:16:02 -0600 Subject: [PATCH 26/26] comments --- Sources/System/FileControl.swift | 58 +++++++------- Sources/System/FileControlTypes.swift | 108 +++++++++++++------------- 2 files changed, 83 insertions(+), 83 deletions(-) diff --git a/Sources/System/FileControl.swift b/Sources/System/FileControl.swift index 99f34783..c9397661 100644 --- a/Sources/System/FileControl.swift +++ b/Sources/System/FileControl.swift @@ -55,11 +55,9 @@ extension FileDescriptor { _ = try control( .setStatusFlags, flags.rawValue, retryOnInterrupt: retryOnInterrupt) } -} -extension FileDescriptor { - /// Get the process ID or process group currently receiv- - /// ing SIGIO and SIGURG signals. + /// Get the process ID or process ID group currently receiving SIGIO and + /// SIGURG signals. /// /// The corresponding C function is `fcntl` with the `F_GETOWN` command. @_alwaysEmitIntoClient @@ -74,8 +72,8 @@ extension FileDescriptor { return (ProcessID(rawValue: pidOrPGID), isGroup: false) } - /// Set the process or process group to receive SIGIO and - /// SIGURG signals. + /// Set the process ID or process group ID to receive SIGIO and SIGURG + /// signals. /// /// The corresponding C function is `fcntl` with the `F_SETOWN` command. @_alwaysEmitIntoClient @@ -96,14 +94,14 @@ extension FileDescriptor { /// - Returns: The lowest numbered available descriptor whose raw value is /// greater than or equal to `minRawValue`. /// - /// File descriptors are merely references to some underlying system resource. - /// The system does not distinguish between the original and the new file - /// descriptor in any way. For example, read, write and seek operations on - /// one of them also affect the logical file position in the other, and - /// append mode, non-blocking I/O and asynchronous I/O options are shared - /// between the references. If a separate pointer into the file is desired, - /// a different object reference to the file must be obtained by issuing an - /// additional call to `open`. + /// File descriptors are merely references to some underlying system + /// resource. The system does not distinguish between the original and the + /// new file descriptor in any way. For example, read, write and seek + /// operations on one of them also affect the logical file position in the + /// other, and append mode, non-blocking I/O and asynchronous I/O options + /// are shared between the references. If a separate pointer into the file + /// is desired, a different object reference to the file must be obtained + /// by issuing an additional call to `open`. /// /// However, each file descriptor maintains its own close-on-exec flag. /// @@ -152,8 +150,8 @@ extension FileDescriptor { } extension FileDescriptor { - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient public func control( _ cmd: Command, retryOnInterrupt: Bool = true @@ -161,8 +159,8 @@ extension FileDescriptor { try _control(cmd, retryOnInterrupt: retryOnInterrupt).get() } - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient public func control( _ cmd: Command, _ arg: CInt, retryOnInterrupt: Bool = true @@ -170,8 +168,8 @@ extension FileDescriptor { try _control(cmd, arg, retryOnInterrupt: retryOnInterrupt).get() } - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient public func control( _ cmd: Command, @@ -181,8 +179,8 @@ extension FileDescriptor { try _control(cmd, ptr, retryOnInterrupt: retryOnInterrupt).get() } - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient public func control( _ cmd: Command, @@ -196,8 +194,8 @@ extension FileDescriptor { // TODO: Worth calling out the command to supply in docs? - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient public func control( _ cmd: Command, @@ -209,8 +207,8 @@ extension FileDescriptor { } } - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient public func control( _ cmd: Command, @@ -222,8 +220,8 @@ extension FileDescriptor { } } - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient public func control( _ cmd: Command, @@ -235,8 +233,8 @@ extension FileDescriptor { } } - /// Low-level interface equivalent to C's `fcntl`. Note, most common operations have Swiftier - /// alternatives directly on `FileDescriptor`. + /// Low-level interface equivalent to C's `fcntl`. Note, most common + /// operations have Swiftier alternatives directly on `FileDescriptor`. @_alwaysEmitIntoClient public func control( _ cmd: Command, diff --git a/Sources/System/FileControlTypes.swift b/Sources/System/FileControlTypes.swift index 29cd59f0..7f98165e 100644 --- a/Sources/System/FileControlTypes.swift +++ b/Sources/System/FileControlTypes.swift @@ -79,83 +79,85 @@ extension FileDescriptor { #if !os(Linux) extension FileDescriptor { - // TODO: Flatten these out? Rename this somehow? + /// Namespace for types used with `FileDescriptor.control`. @frozen - public enum ControlTypes { - /// The corresponding C type is `fstore`. - @frozen - public struct Store: RawRepresentable, Sendable { - @_alwaysEmitIntoClient - public var rawValue: CInterop.FStore - - @_alwaysEmitIntoClient - public init(rawValue: CInterop.FStore) { self.rawValue = rawValue } + public enum ControlTypes { } +} - @frozen - public struct Flags: OptionSet, Sendable { - @_alwaysEmitIntoClient - public var rawValue: UInt32 +extension FileDescriptor.ControlTypes { + /// The corresponding C type is `fstore`. + @frozen + public struct Store: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FStore - @_alwaysEmitIntoClient - public init(rawValue: UInt32) { self.rawValue = rawValue } - } + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FStore) { self.rawValue = rawValue } - @frozen - public struct PositionMode: RawRepresentable, Hashable, Sendable { - @_alwaysEmitIntoClient - public var rawValue: CInt + @frozen + public struct Flags: OptionSet, Sendable { + @_alwaysEmitIntoClient + public var rawValue: UInt32 - @_alwaysEmitIntoClient - public init(rawValue: CInt) { self.rawValue = rawValue } - } + @_alwaysEmitIntoClient + public init(rawValue: UInt32) { self.rawValue = rawValue } } - /// The corresponding C type is `fpunchhole` @frozen - public struct Punchhole: RawRepresentable, Sendable { + public struct PositionMode: RawRepresentable, Hashable, Sendable { @_alwaysEmitIntoClient - public var rawValue: CInterop.FPunchhole + public var rawValue: CInt @_alwaysEmitIntoClient - public init(rawValue: CInterop.FPunchhole) { self.rawValue = rawValue } + public init(rawValue: CInt) { self.rawValue = rawValue } + } + } - @frozen - public struct Flags: OptionSet, Sendable { - @_alwaysEmitIntoClient - public var rawValue: UInt32 + /// The corresponding C type is `fpunchhole` + @frozen + public struct Punchhole: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.FPunchhole - @_alwaysEmitIntoClient - public init(rawValue: UInt32) { self.rawValue = rawValue } - } - } + @_alwaysEmitIntoClient + public init(rawValue: CInterop.FPunchhole) { self.rawValue = rawValue } - /// The corresponding C type is `radvisory` @frozen - public struct ReadAdvisory: RawRepresentable, Sendable { + public struct Flags: OptionSet, Sendable { @_alwaysEmitIntoClient - public var rawValue: CInterop.RAdvisory + public var rawValue: UInt32 @_alwaysEmitIntoClient - public init(rawValue: CInterop.RAdvisory) { self.rawValue = rawValue } + public init(rawValue: UInt32) { self.rawValue = rawValue } } + } + + /// The corresponding C type is `radvisory` + @frozen + public struct ReadAdvisory: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.RAdvisory + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.RAdvisory) { self.rawValue = rawValue } + } + + /// The corresponding C type is `log2phys` + @frozen + public struct LogicalToPhysical: RawRepresentable, Sendable { + @_alwaysEmitIntoClient + public var rawValue: CInterop.Log2Phys + + @_alwaysEmitIntoClient + public init(rawValue: CInterop.Log2Phys) { self.rawValue = rawValue } - /// The corresponding C type is `log2phys` @frozen - public struct LogicalToPhysical: RawRepresentable, Sendable { + public struct Flags: OptionSet, Sendable { @_alwaysEmitIntoClient - public var rawValue: CInterop.Log2Phys + public let rawValue: UInt32 @_alwaysEmitIntoClient - public init(rawValue: CInterop.Log2Phys) { self.rawValue = rawValue } - - @frozen - public struct Flags: OptionSet, Sendable { - @_alwaysEmitIntoClient - public let rawValue: UInt32 - - @_alwaysEmitIntoClient - public init(rawValue: UInt32) { self.rawValue = rawValue } - } + public init(rawValue: UInt32) { self.rawValue = rawValue } } } }