Random hang by macOS task/process when specifying stderr pipe

70 views Asked by At

Note: This is believed to be similar to cURL through NSTask not terminating if a pipe is present, on which I recently placed a 200 point bounty. I'm posting here in order to provide additional details of my own case.

When pasted into the swift REPL (using Swift 3), the following code randomly hangs with the associated files present on the MacOS file system, with probability greater than 50% on my machine:

import Foundation

func test() {
    func innerTest(filenames: String...) {
        let task = Process()
        let pipe = Pipe()
        let choice = true ? 1 : Int(arc4random_uniform(2))
        let dir = ["/tmp", "/tmp/a/b/c/d/e/f/g/"][choice]
        print(dir)
        task.launchPath = "/usr/bin/swiftc"
        let inputPaths = filenames.map { dir + $0 + ".swift" }
        let outputFile = "/tmp/" + filenames.joined() + ".ast"
        task.arguments = ["-dump-ast"] + inputPaths
        task.standardError = pipe
        task.launch()
        task.waitUntilExit()
        try! String(data: pipe.fileHandleForReading.readDataToEndOfFile(), encoding: String.Encoding.utf8)!.write(toFile: outputFile, atomically: true, encoding: String.Encoding.utf8)
    }

    innerTest(filenames: "AST", "Token")
}

test()

As with the other stackoverflow question, the hang never occurs unless the pipe is set on the task/process.

The code for selecting between /tmp and /tmp/a/b/c/d/e/f/g as source of the input files is present because it will never hang when using /tmp as input and will randomly hang when using the longer directory path. I left it in there to allow one to experiment.

The contents of AST.swift is:

struct AST {
    let type: String
    let implicit: Bool
    let name: String?
    let attributes: [String:String]
    let elements: [AST]

    init(type: String, implicit: Bool = false, name: String? = nil, attributes: [String:String] = [:], elements: [AST] = []) {
        self.type = type
        self.implicit = implicit
        self.name = name
        self.attributes = attributes
        self.elements = elements
    }
}

The contents of Token.swift is:

class Token: Equatable {
    private class LeftParen: Token {}
    static let leftParen: Token = LeftParen()
    private class RightParen: Token {}
    static let rightParen: Token = RightParen()

    class String: Token {
        let value: Swift.String
        init(_ value: Swift.String) { self.value = value }
    }

    class Symbol: Token {
        let value: Swift.String
        init(_ value: Swift.String) { self.value = value }
    }

    class KeyValue: Token {
        let key: Swift.String
        let value: Swift.String
        init(_ key: Swift.String, _ value: Swift.String) {
            self.key = key
            self.value = value
        }
    }

    static func symbol(_ s: Swift.String) -> Symbol { return Symbol(s) }
    static func string(_ s: Swift.String) -> String { return String(s) }
    static func keyValue(_ k: Swift.String, _ v: Swift.String) -> KeyValue { return KeyValue(k, v) }

    func toString() -> Swift.String {
        switch self {
        case Token.leftParen:
            return "("
        case Token.rightParen:
            return ")"
        case let token as Token.String:
            return token.value
        case let token as Token.Symbol:
            return token.value
        case let token as Token.KeyValue:
            return "\(token.key)=\(token.value)"
        default:
            fatalError()
        }
    }
}

func ==(lhs: Token, rhs: Token) -> Bool {
    if let lhs = lhs as? Token.String, let rhs = rhs as? Token.String {
        return lhs.value == rhs.value
    }
    if let lhs = lhs as? Token.Symbol, let rhs = rhs as? Token.Symbol {
        return lhs.value == rhs.value
    }
    if let lhs = lhs as? Token.KeyValue, let rhs = rhs as? Token.KeyValue {
        return lhs.key == rhs.key && lhs.value == rhs.value
    }
    return lhs === rhs
}

The above two .swift files need to be placed into a /tmp/a/b/c/d/e/f/g directory in order for the test to run. If you want to see it always succeed when using /tmp as a source of input, they need to be copied there as well.

0

There are 0 answers