1

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.

Community
  • 1
  • 1
Peter Alfvin
  • 28,599
  • 8
  • 68
  • 106
  • 1
    My assumption is: The "swiftc" process blocks because the pipe if full and it cannot write all its standard error. If you `waitUntilExit()` *after* reading from the pipe then it does not hang in my test. – However that would not explain why it happens only with /tmp/a/b/c/d/e/f/g and not with /tmp. – Martin R Jan 08 '17 at 21:24
  • @MartinR Indeed. Thanks so much. Not that you appear to need it ;-), but I'll gladly award you the bounty if you want to answer that other question. – Peter Alfvin Jan 08 '17 at 21:33
  • Well, I could not reproduce the problem in the other question, and I think the existing answer already goes into that direction. Also it does not fully explain the problem because it happens only with /tmp/a/b/c/d/e/f/g and not with /tmp. – Martin R Jan 08 '17 at 21:37
  • 1
    Turns out the the size of the output generated by the swift compiler is a function of the length of the input paths, so the longer they get, the more output is generated. That would explain why the buffer is more quickly exhausted in one case vs. the other. – Peter Alfvin Jan 08 '17 at 21:40
  • Excellent point! It is 64114 vs 71226 bytes, so the pipe size could be 64K. – Martin R Jan 08 '17 at 21:44
  • Oh, and thanks for editing out the paste errors in this question. Sorry for the sloppiness. – Peter Alfvin Jan 08 '17 at 21:49
  • I have added an answer to the other question. But note that @Hod essentially explained the problem. – *This* question can be closed as duplicate of the other one, I think. – Martin R Jan 08 '17 at 22:46
  • I just realized I can't split my bounty, which was what I was going to do in light of your remarks. Do you have a preference as to which of you gets it? I'm inclined to give to @Hod on the grounds that it would be more meaningful to him. – Peter Alfvin Jan 08 '17 at 23:33
  • My analysis was headed in the right direction, but my suggested solution wasn't helpful, since it would address buffering only on the output generation side. @MartinR really nailed it in the end. I'm disappointed I didn't completely crack it, but I'm happy with the up votes. – Hod Jan 09 '17 at 01:38

0 Answers0