Merge pull request #15323 from obsidiansystems/fix-eio-readline

libutil: treat EIO as EOF in `readLine`
This commit is contained in:
John Ericson
2026-02-23 19:30:19 +00:00
committed by GitHub
2 changed files with 66 additions and 2 deletions

View File

@@ -7,6 +7,11 @@
#include <cstring>
#ifndef _WIN32
# include <fcntl.h>
# include <stdlib.h>
#endif
namespace nix {
// BufferedSource with configurable small buffer for precise boundary testing.
@@ -114,6 +119,55 @@ TEST(ReadLine, LineWithNullBytes)
3));
}
#ifndef _WIN32
TEST(ReadLine, TreatsEioAsEof)
{
// Open a pty master. When the slave side is closed (or never opened),
// reading from the master returns EIO, which readLine should treat as EOF.
int master = posix_openpt(O_RDWR | O_NOCTTY);
ASSERT_NE(master, -1);
ASSERT_EQ(grantpt(master), 0);
ASSERT_EQ(unlockpt(master), 0);
// Open and immediately close the slave to trigger EIO on the master.
int slave = open(ptsname(master), O_RDWR | O_NOCTTY);
ASSERT_NE(slave, -1);
close(slave);
// With eofOk=true, readLine should return empty string (treating EIO as EOF).
EXPECT_EQ(readLine(master, /*eofOk=*/true), "");
// With eofOk=false, readLine should throw EndOfFile.
EXPECT_THROW(readLine(master), EndOfFile);
close(master);
}
// macOS (BSD) discards buffered pty data on slave close and returns normal
// EOF (0) instead of EIO, so partial data never reaches the master.
# ifdef __linux__
TEST(ReadLine, PartialLineBeforeEio)
{
int master = posix_openpt(O_RDWR | O_NOCTTY);
ASSERT_NE(master, -1);
ASSERT_EQ(grantpt(master), 0);
ASSERT_EQ(unlockpt(master), 0);
int slave = open(ptsname(master), O_RDWR | O_NOCTTY);
ASSERT_NE(slave, -1);
// Write a partial line (no terminator) from the slave, then close it.
ASSERT_EQ(::write(slave, "partial", 7), 7);
close(slave);
// readLine should return the partial data when eofOk=true.
EXPECT_EQ(readLine(master, /*eofOk=*/true), "partial");
close(master);
}
# endif
#endif
TEST(BufferedSourceReadLine, ReadsLinesFromPipe)
{
Pipe pipe;

View File

@@ -76,8 +76,18 @@ std::string readLine(Descriptor fd, bool eofOk, char terminator)
checkInterrupt();
char ch;
// FIXME: inefficient
auto rd =
retryOnBlock(fd, PollDirection::In, [&]() { return read(fd, {reinterpret_cast<std::byte *>(&ch), 1}); });
auto rd = retryOnBlock(fd, PollDirection::In, [&]() -> size_t {
try {
return read(fd, {reinterpret_cast<std::byte *>(&ch), 1});
} catch (SystemError & e) {
// On pty masters, EIO signals that the slave side closed,
// which is semantically EOF. Map it to a zero-length read
// so the existing EOF path handles it.
if (e.is(std::errc::io_error))
return 0;
throw;
}
});
if (rd == 0) {
if (eofOk)
return s;