Skip to content

Add a Pipe type. #694

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

Merged
merged 4 commits into from
Sep 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
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
113 changes: 113 additions & 0 deletions Sources/Testing/Support/FileHandle.swift
Original file line number Diff line number Diff line change
Expand Up @@ -120,12 +120,54 @@ struct FileHandle: ~Copyable, Sendable {
_closeWhenDone = closeWhenDone
}

/// Initialize an instance of this type with an existing POSIX file descriptor
/// for reading.
///
/// - Parameters:
/// - fd: The POSIX file descriptor to wrap. The caller is responsible for
/// ensuring that this file handle is open in the expected mode and that
/// another part of the system won't close it.
/// - mode: The mode `fd` was opened with, such as `"wb"`.
///
/// - Throws: Any error preventing the stream from being opened.
///
/// The resulting file handle takes ownership of `fd` and closes it when it is
/// deinitialized or if an error is thrown from this initializer.
init(unsafePOSIXFileDescriptor fd: CInt, mode: String) throws {
#if os(Windows)
let fileHandle = _fdopen(fd, mode)
#else
let fileHandle = fdopen(fd, mode)
#endif
guard let fileHandle else {
let errorCode = swt_errno()
#if os(Windows)
_close(fd)
#else
_TestingInternals.close(fd)
#endif
throw CError(rawValue: errorCode)
}
self.init(unsafeCFILEHandle: fileHandle, closeWhenDone: true)
}

deinit {
if _closeWhenDone {
fclose(_fileHandle)
}
}

/// Close this file handle.
///
/// This function effectively deinitializes the file handle.
///
/// - Warning: This function closes the underlying C file handle even if
/// `closeWhenDone` was `false` when this instance was initialized. Callers
/// must take care not to close file handles they do not own.
consuming func close() {
_closeWhenDone = true
}

/// Call a function and pass the underlying C file handle to it.
///
/// - Parameters:
Expand Down Expand Up @@ -383,6 +425,77 @@ extension FileHandle {
}
}

// MARK: - Pipes

extension FileHandle {
/// A type representing a bidirectional pipe between two file handles.
struct Pipe: ~Copyable, Sendable {
/// The end of the pipe capable of reading.
var readEnd: FileHandle

/// The end of the pipe capable of writing.
var writeEnd: FileHandle

/// Initialize a new anonymous pipe.
///
/// - Throws: Any error that prevented creation of the pipe.
init() throws {
let (fdReadEnd, fdWriteEnd) = try withUnsafeTemporaryAllocation(of: CInt.self, capacity: 2) { fds in
#if os(Windows)
guard 0 == _pipe(fds.baseAddress, 0, _O_BINARY) else {
throw CError(rawValue: swt_errno())
}
#else
guard 0 == pipe(fds.baseAddress) else {
throw CError(rawValue: swt_errno())
}
#endif
return (fds[0], fds[1])
}

// NOTE: Partial initialization of a move-only type is disallowed, as is
// conditional initialization of a local move-only value, which is why
// this section looks a little awkward.
let readEnd: FileHandle
do {
readEnd = try FileHandle(unsafePOSIXFileDescriptor: fdReadEnd, mode: "rb")
} catch {
#if os(Windows)
_close(fdWriteEnd)
#else
_TestingInternals.close(fdWriteEnd)
#endif
throw error
}
let writeEnd = try FileHandle(unsafePOSIXFileDescriptor: fdWriteEnd, mode: "wb")
self.readEnd = readEnd
self.writeEnd = writeEnd
}

/// Close the read end of this pipe.
///
/// - Returns: The remaining open end of the pipe.
///
/// After calling this function, the read end is closed and the write end
/// remains open.
consuming func closeReadEnd() -> FileHandle {
readEnd.close()
return writeEnd
}

/// Close the write end of this pipe.
///
/// - Returns: The remaining open end of the pipe.
///
/// After calling this function, the write end is closed and the read end
/// remains open.
consuming func closeWriteEnd() -> FileHandle {
writeEnd.close()
return readEnd
}
}
}

// MARK: - Attributes

extension FileHandle {
Expand Down
72 changes: 55 additions & 17 deletions Tests/TestingTests/Support/FileHandleTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,15 @@ struct FileHandleTests {
}
}

#if !os(Windows) // Windows does not like invalid file descriptors.
@Test("Init from invalid file descriptor")
func invalidFileDescriptor() throws {
#expect(throws: CError.self) {
_ = try FileHandle(unsafePOSIXFileDescriptor: -1, mode: "")
}
}
#endif

#if os(Windows)
@Test("Can get Windows file HANDLE")
func fileHANDLE() throws {
Expand All @@ -54,6 +63,16 @@ struct FileHandleTests {
}
#endif

#if SWT_TARGET_OS_APPLE
@Test("close() function")
func closeFunction() async throws {
try await confirmation("File handle closed") { closed in
let fileHandle = try fileHandleForCloseMonitoring(with: closed)
fileHandle.close()
}
}
#endif

@Test("Can write to a file")
func canWrite() throws {
try withTemporaryPath { path in
Expand Down Expand Up @@ -132,25 +151,25 @@ struct FileHandleTests {

@Test("Can recognize opened pipe")
func isPipe() throws {
#if os(Windows)
var rHandle: HANDLE?
var wHandle: HANDLE?
try #require(CreatePipe(&rHandle, &wHandle, nil, 0))
if let rHandle {
CloseHandle(rHandle)
let pipe = try FileHandle.Pipe()
#expect(pipe.readEnd.isPipe as Bool)
#expect(pipe.writeEnd.isPipe as Bool)
}

#if SWT_TARGET_OS_APPLE
@Test("Can close ends of a pipe")
func closeEndsOfPipe() async throws {
try await confirmation("File handle closed", expectedCount: 2) { closed in
var pipe1 = try FileHandle.Pipe()
pipe1.readEnd = try fileHandleForCloseMonitoring(with: closed)
_ = pipe1.closeReadEnd()

var pipe2 = try FileHandle.Pipe()
pipe2.writeEnd = try fileHandleForCloseMonitoring(with: closed)
_ = pipe2.closeWriteEnd()
}
let fdWrite = _open_osfhandle(intptr_t(bitPattern: wHandle), 0)
let file = try #require(_fdopen(fdWrite, "wb"))
#else
var fds: [CInt] = [-1, -1]
try #require(0 == pipe(&fds))
try #require(fds[1] >= 0)
close(fds[0])
let file = try #require(fdopen(fds[1], "wb"))
#endif
let fileHandle = FileHandle(unsafeCFILEHandle: file, closeWhenDone: true)
#expect(Bool(fileHandle.isPipe))
}
#endif

@Test("/dev/null is not a TTY or pipe")
func devNull() throws {
Expand Down Expand Up @@ -239,4 +258,23 @@ func temporaryDirectory() throws -> String {
#endif
}

#if SWT_TARGET_OS_APPLE
func fileHandleForCloseMonitoring(with confirmation: Confirmation) throws -> FileHandle {
let context = Unmanaged.passRetained(confirmation as AnyObject).toOpaque()
let file = try #require(
funopen(
context,
{ _, _, _ in 0 },
nil,
nil,
{ context in
let confirmation = Unmanaged<AnyObject>.fromOpaque(context!).takeRetainedValue() as! Confirmation
confirmation()
return 0
}
) as SWT_FILEHandle?
)
return FileHandle(unsafeCFILEHandle: file, closeWhenDone: false)
}
#endif
#endif