Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement FileDescriptor.Pipe() #58

Merged
merged 17 commits into from
Sep 27, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Sources/System/FileHelpers.swift
Original file line number Diff line number Diff line change
Expand Up @@ -123,3 +123,34 @@ extension FileDescriptor {
}
}
}

extension FileDescriptor.Pipe {
/// Runs a closure and then closes both ends of this pipe, even if an error occurs.
///
/// - Parameter body: The closure to run.
/// If the closure throws an error,
/// this method closes both ends of this pipe before it rethrows that error.
///
/// - Returns: The value returned by the closure.
///
/// If `body` throws an error
/// or an error occurs while closing the file descriptor,
/// this method rethrows that error.
public func closeAfter<R>(_ body: () throws -> R) throws -> R {
GeorgeLyon marked this conversation as resolved.
Show resolved Hide resolved
try fileDescriptorForReading.closeAfter {
try fileDescriptorForWriting.closeAfter {
try body()
}
}
}

/// Closes both ends of a Pipe
///
/// If both ends fail to close, the error from the read end is thrown.
@_alwaysEmitIntoClient
public func close() throws {
GeorgeLyon marked this conversation as resolved.
Show resolved Hide resolved
try fileDescriptorForWriting.closeAfter {
try fileDescriptorForReading.close()
}
}
}
48 changes: 48 additions & 0 deletions Sources/System/FileOperations.swift
Original file line number Diff line number Diff line change
Expand Up @@ -370,4 +370,52 @@ extension FileDescriptor {
public func dup2() throws -> FileDescriptor {
fatalError("Not implemented")
}

/// A pair of `FileDescriptor` values represening a pipe.
///
/// You are responsible for managing the lifetime and validity
/// of the `read` and `write` `FileDescriptor` values,
/// which can be closed independently or via helper methods on `Pipe`, but not both.
public struct Pipe {
GeorgeLyon marked this conversation as resolved.
Show resolved Hide resolved
/// The file descriptor for reading from this pipe
public let fileDescriptorForReading: FileDescriptor
GeorgeLyon marked this conversation as resolved.
Show resolved Hide resolved

/// The file descriptor for writing to this pipe
public let fileDescriptorForWriting: FileDescriptor
GeorgeLyon marked this conversation as resolved.
Show resolved Hide resolved

/// Create a pipe, a uniderctional data channel which can be used for interprocess communication.
///
/// - Returns: A `Pipe` value representing the read and write ends of the created `Pipe`.
///
/// The corresponding C function is `pipe`.
@_alwaysEmitIntoClient
// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
public init(
retryOnInterrupt: Bool = true
) throws {
self = try Self._create(retryOnInterrupt: retryOnInterrupt).get()
}

// @available(macOS 9999, iOS 9999, watchOS 9999, tvOS 9999, *)
@usableFromInline
internal static func _create(
retryOnInterrupt: Bool
) -> Result<Pipe, Errno> {
var fds: [Int32] = [-1, -1]
GeorgeLyon marked this conversation as resolved.
Show resolved Hide resolved
return valueOrErrno(retryOnInterrupt: retryOnInterrupt) {
system_pipe(&fds)
}.map { _ in Pipe(fds: fds) }
}

private init(fds: [Int32]) {
GeorgeLyon marked this conversation as resolved.
Show resolved Hide resolved
fileDescriptorForReading = FileDescriptor(rawValue: fds[0])
fileDescriptorForWriting = FileDescriptor(rawValue: fds[1])
}
}

@_alwaysEmitIntoClient
@available(*, unavailable, renamed: "FileDescriptor.Pipe()")
public func pipe() throws -> FileDescriptor {
fatalError("Not implemented")
}
}
8 changes: 8 additions & 0 deletions Sources/System/Internals/Syscalls.swift
Original file line number Diff line number Diff line change
Expand Up @@ -115,3 +115,11 @@ internal func system_dup2(_ fd: Int32, _ fd2: Int32) -> Int32 {
#endif
return dup2(fd, fd2)
}

// write
GeorgeLyon marked this conversation as resolved.
Show resolved Hide resolved
internal func system_pipe(_ fds: UnsafeMutablePointer<Int32>) -> CInt {
#if ENABLE_MOCKING
if mockingEnabled { return _mock(fds) }
#endif
return pipe(fds)
}
19 changes: 19 additions & 0 deletions Tests/SystemTests/FileOperationsTest.swift
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ final class FileOperationsTest: XCTestCase {
func testHelpers() {
// TODO: Test writeAll, writeAll(toAbsoluteOffset), closeAfter
}

func testAdHocPipe() throws {
// Ad-hoc test testing `Pipe` functionality.
// We cannot test `Pipe` using `MockTestCase` because it calls `pipe` with a pointer to an array local to the `Pipe`, the address of which we do not know prior to invoking `Pipe`.
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We probably do (not gonna hold up this PR for it though) want to allow for mock testing of such stack local pointers. We'd probably have a variant that we'd pass in an array and it would compare the contents.

let pipe = try FileDescriptor.Pipe()
try pipe.fileDescriptorForWriting.closeAfter {
try pipe.fileDescriptorForReading.closeAfter {
var abc = "abc"
try abc.withUTF8 {
_ = try pipe.fileDescriptorForWriting.write(UnsafeRawBufferPointer($0))
}
let readLen = 3
let readBytes = try Array<UInt8>(unsafeUninitializedCapacity: readLen) { buf, count in
count = try pipe.fileDescriptorForReading.read(into: UnsafeMutableRawBufferPointer(buf))
}
XCTAssertEqual(readBytes, Array(abc.utf8))
}
}
}

func testAdHocOpen() {
// Ad-hoc test touching a file system.
Expand Down