pipe ( ) doesn't seem to work when running app natively

(Xcode 10.1, Apple Swift version 4.2.1 (swiftlang-1000.11.42 clang-1000.11.45.1))


I'm developing an App (my first one) and during the development process I'd rather have a debug window into which I can write messages for development and debugging purposes.


I have a UItextView for this and to get all print() into that textView, I'm using the following construct (pipe()) inspired by this:


//
//  ViewController.swift
//  Scroll View Demo
//
//  Created by chriskuku on 30.12.18.
//  Copyright © 2018 chriskuku. All rights reserved.
//

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.
        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) ?? "\n"
            DispatchQueue.main.async {
                self?.textView.text += str
            }
        }
        print("\npipe started")
    }

    @IBAction func buttonPressed(_ sender: Any) {
        print("\(count). Hello world")
        count += 1
    }
}



The App works fine when being run within Xcode on either virtual device (iPhone 6s plus in my case) or physially on the device connected to Xcode.


But when I run the app alone on the target device (Apple iPhone 6splus), the pipe() mechanism doesn't seem to work. Nothing appears but the Initial text.


I'm puzzled.

Accepted Reply

The problem here is that you’re not flushing

stdout
. When your run your app from Xcode,
stdout
is connected to a pseudo terminal and thus configures itself to be unbuffered. When you run your app from the Home screen,
stdout
is connected to something else (
/dev/null
IIRC) and thus ends up buffered. So the data you print is never flushed to the pipe, and thus never shows up in your readability handler.

You can fix your current code by adding the following line to your setup code.

setvbuf(stdout, nil, _IONBF, 0)

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Replies

The problem here is that you’re not flushing

stdout
. When your run your app from Xcode,
stdout
is connected to a pseudo terminal and thus configures itself to be unbuffered. When you run your app from the Home screen,
stdout
is connected to something else (
/dev/null
IIRC) and thus ends up buffered. So the data you print is never flushed to the pipe, and thus never shows up in your readability handler.

You can fix your current code by adding the following line to your setup code.

setvbuf(stdout, nil, _IONBF, 0)

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

Fantastic! And to be sure: the setvbuf() should occur *before* the dup2 occurs, on the original filehandle, right? At least it works this way with me here. While trying to make it work with the pipe() filehandle, I'm getting into trouble with the type passed to the setvbuf().



setvbuf(pipe.fileHandleForReading, nil, _IONBF, 0)


gives me an error:


Cannot convert value of type 'FileHandle' to expected argument type 'UnsafeMutablePointer<FILE>?' (aka 'Optional<UnsafeMutablePointer<__sFILE>>')

And to be sure: the setvbuf() should occur before the

dup2
occurs, on the original filehandle, right?

It doesn’t matter. Buffering is done by the

FILE *
not the file descriptor.

I'm getting into trouble with the type passed to the

setvbuf
.

You’re definitely heading off into the weeds here )-: Buffering is done by the

FILE *
— that is,
stdout
in this example — not by the file descriptor (
STDOUT_FILENO
) or your file handle. Thus you need to call
setvbuf
as I showed in my last post.

Here’s how thing progress in that case:

  1. Your Swift code calls

    print
    .
  2. Internally

    print
    calls
    fwrite
    passing it
    stdout
    .
  3. fwrite
    decides to buffer or not based on the
    setvbuf
    configuration:
    • In the unbuffered case,

      fwrite
      calls
      write
      with
      STDOUT_FILENO
      .
    • If the buffered case,

      fwrite
      copies the data to the buffer and, only if it’s full, call
      write
      with
      STDOUT_FILENO
      .

So you only need to change buffering on

stdout
, the
FILE *
, and not on the file descriptor, the file handle, the Swift
TextOutputStreamable
, or anything else.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"

I'm now observing, that I'm also getting kind of unwanted "debug messages" from iOS in my app, since I redirected stdout and stderr to my textView:


2019-01-16 11:38:11.144848+0100 Scroll View Demo[6044:2097913] -[UIWindow endDisablingInterfaceAutorotationAnimated:] called on > without matching -beginDisablingInterfaceAutorotation. Ignoring.


I didn't order this message 🙂 Is it a left over of some Apple Developer having scaffolded the iOS in this place?

It’s not uncommon for the OS to log messages like this. In the most egregious cases I recommend that folks file a bug about the log noise. For the moment, however, you’re going to have to decide how you want to handle this reality, and that boils down to three options:

  • Change the code you use to print your messages to use something other than just a simple

    print
    . You could, for example, use the
    to
    parameter to print to a custom
    TextOutputStream
    that redirects your output to the right place.
  • Continue with your current design but filter out this system output.

  • Accept the fact that this system output will show up in your debug window.

Share and Enjoy

Quinn “The Eskimo!”
Apple Developer Relations, Developer Technical Support, Core OS/Hardware

let myEmail = "eskimo" + "1" + "@apple.com"
I did flush the stdout by adding this line
  • -> setvbuf(stdout, nil, IONBF, 0)

But still nothing display on the text view. Only happens in release mode.

func displayOutput (
task:Process) {

        let outputPipe = Pipe()
        setvbuf(stdout, nil, IONBF, 0)
        dup2(outputPipe.fileHandleForReading.fileDescriptor, STDOUT
FILENO)
        task.standardOutput = outputPipe
        outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()

        NotificationCenter.default.addObserver(forName: NSNotification.Name.NSFileHandleDataAvailable, object: outputPipe.fileHandleForReading , queue: nil) { notification in
            let output = outputPipe.fileHandleForReading.availableData
            let outputString = String(data: output, encoding: String.Encoding.utf8) ?? ""

            if outputString.count > 0 {
                DispatchQueue.main.async(execute: {
                    let previousOutput = self.outputTextView.string
                    let nextOutput = previousOutput + "\n" + outputString
                    self.outputTextView.string = nextOutput

                    let range = NSRange(location:nextOutput.count,length:0)
                    self.outputTextView.scrollRangeToVisible(range)
                })
            }
            outputPipe.fileHandleForReading.waitForDataInBackgroundAndNotify()
        }
    }
I recommend that you start a new thread for this. The issue you’re having is only tangentially related to the issue from the original post (because your snippet involves Process).

When you create the new thread:
  • Tag it Foundation, which represents the majority of the API you’re using (Task, FileHandle, and so on).

  • Put your code in code block (via the <>) button. That’ll make it much easier to read (for example, you won’t lose all the underscores).

  • Tell us more about the context for this code, most critically, the tool you’re running and what you expect it to output.

Share and Enjoy

Quinn “The Eskimo!” @ Developer Technical Support @ Apple
let myEmail = "eskimo" + "1" + "@apple.com"