Krischu
Krischu

Reputation: 1125

using pipe() in Swift App to redirect stdout into a textView (only runs in simulator, not native)

I created an iOS app for iPhone using Swift under the Xcode IDE. For debugging purposes I would like to have everything what print (and printf in C code) prints to the Xcode console normally, redirected to a UItextView. My code (mainly derived from here) follows:

//
//  ViewController.swift
//  Scroll View Demo
//

//  
//

import UIKit

class ViewController: UIViewController {


    @IBOutlet weak var textView: UITextView!
    @IBOutlet weak var writeButton: UIButton!
    var pipe = Pipe()
    var count = 0

    override func viewDidLoad() {
        super.viewDidLoad()
        // Do any additional setup after loading the view, typically from a nib.
        // dup2() makes newfd (new file descriptor) be the copy of oldfd (old file descriptor), closing newfd first if necessary.
        openConsolePipe()
        print("\npipe started")

    }

    @IBAction func buttonPressed(_ sender: Any) {
        print("\(count). Hello world")
        count += 1
    }
    public func openConsolePipe () {
        dup2(pipe.fileHandleForWriting.fileDescriptor, 
            STDOUT_FILENO)
        // listening on the readabilityHandler
        pipe.fileHandleForReading.readabilityHandler = {
         [weak self] handle in
        let data = handle.availableData
        let str = String(data: data, encoding: .ascii) ?? "<Non-ascii data of size\(data.count)>\n"
        DispatchQueue.main.async {
            self?.textView.text += str
        }
      }
    }

}

The code works in simulation mode on the physical device and in the virtual iphone as well. But when I run it natively with the cord to Xcode cut, the pipe() doesn't work. I'm clueless at the moment why the pipe() doesn't work in the standalone app.

Upvotes: 8

Views: 3912

Answers (1)

Krischu
Krischu

Reputation: 1125

The code change that made the example finally work is to modify the file handle of stdout to unbuffered (setvbuf(stdout, nil, _IONBF, 0)):

//
//  ViewController.swift
//  Scroll View Demo
//

//  
//



import UIKit

class ViewController: UIViewController {


@IBOutlet weak var textView: UITextView!
@IBOutlet weak var writeButton: UIButton!
var pipe = Pipe()
var count = 0

override func viewDidLoad() {
    super.viewDidLoad()
    // Do any additional setup after loading the view, typically from a nib.
    // dup2() makes newfd (new file descriptor) be the copy of oldfd (old file descriptor), closing newfd first if necessary.
    openConsolePipe()
    print("\npipe started")

}

@IBAction func buttonPressed(_ sender: Any) {
    print("\(count). Hello world")
    count += 1
}
public func openConsolePipe () {
    setvbuf(stdout, nil, _IONBF, 0) //<--------- !
    dup2(pipe.fileHandleForWriting.fileDescriptor, 
        STDOUT_FILENO)
    // listening on the readabilityHandler
    pipe.fileHandleForReading.readabilityHandler = {
     [weak self] handle in
    let data = handle.availableData
    let str = String(data: data, encoding: .ascii) ?? "<Non-ascii data of size\(data.count)>\n"
    DispatchQueue.main.async {
        self?.textView.text += str
    }
  }
}

That's because with the app running disconnected from Xcode stdout is redirected to something like /dev/null with buffering set to "buffered" and thus never appears in the pipe(). Setting it to unbuffered made things work.

Upvotes: 8

Related Questions