Skip to content

[SR-12080] readabilityHandler on pipe that is Process.standardError sometimes doesn't get called on EOF #3275

Open
@weissi

Description

@weissi
Previous ID SR-12080
Radar rdar://problem/58997201
Original Reporter @weissi
Type Bug

Attachment: Download

Additional Detail from JIRA
Votes 1
Component/s Foundation
Labels Bug, Linux
Assignee None
Priority Medium

md5: cd028eabf8a3fd19f398f6e16e95a8e4

Issue Description:

description

The following program just repeatedly spawns /usr/bin/env and waits for both the stdout & stderr pipes to call the readabilityHandler with a handle.availableData.isEmpty (which means EOF).

On macOS it works just fine, on Linux it usually gets stuck fairly soon and it's always missing the call with handle.availableData.isEmpty of stderr. We've never seen an invocation where the readabilityHandler wasn't called for stdout.

This affects at least Swift 5.1, 5.2, and the latest master snapshot.

Credit to rignatus (JIRA User) for finding the initial issue that led to us debugging this together.

program

import Foundation

for i in 1..<1_000_000 {
    fputs(".", stdout)
    fflush(stdout)
    if i % 10 == 0 {
        fputs("\n", stdout)
    }

    let process = Process()
    process.executableURL = URL(fileURLWithPath: "/usr/bin/env")
    let g = DispatchGroup()
    
    let stdoutPipe = Pipe()
    let stderrPipe = Pipe()
    process.standardOutput = stdoutPipe
    process.standardError = stderrPipe
    g.enter()
    g.enter()
    var numberOfCallsOut = 0
    var numberOfCallsErr = 0
    stderrPipe.fileHandleForReading.readabilityHandler = { handle in
        if handle.availableData.isEmpty {
            fputs("o", stdout)
            fflush(stdout)
            numberOfCallsOut += 1
            //precondition(numberOfCalls == 1, "numberOfCalls: \(numberOfCalls)")
            if numberOfCallsOut == 1 { g.leave() }
        }
    }
    stdoutPipe.fileHandleForReading.readabilityHandler = { handle in
        if handle.availableData.isEmpty {
            fputs("e", stdout)
            fflush(stdout)
            numberOfCallsErr += 1
            //precondition(numberOfCalls == 1, "numberOfCalls: \(numberOfCalls)")
            if numberOfCallsErr == 1 { g.leave() }
        }
    }

    try! process.run()
    g.wait()
}

analysis

We did check the following things:

  • the process actually exited

  • the write end of the pipe is not open anymore (we checked by attaching lldb and calling read on it which promptly returned 0. We also checked ls -la /proc//fd/ and lsof -p PID)

repro

on Linux

noformat
swift File.swift && ./File
noformat

on macOS (via docker)

# assuming File.swift is in the working directory
docker run --privileged --rm -v "$PWD:$PWD" -w "$PWD" -it norionomura/swift:nightly bash -c 'swiftc File.swift && ./File'

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions