Search code examples
phpcblockingproc-open

PHP proc_open blocking reads to stdout until C program terminates


I'm working on an app that uses a fingerprint scanner for user identification. Interacting with the fingerprint scanner is done in a C program with libfprint that I invoke using proc_open in PHP. The fingerprint enrollment process is a multi-stage process handled by libprintf as a state machine; control is passed between the library and the C program to allow the C program to provide feedback to the user (stage successful, unsuccessful, and why)...

The problem comes in when trying to read the data from the process resource in PHP. Sample PHP code here:

<?php
ob_implicit_flush(true);
$cmd = "/home/pi/projects/PingPongSet/enroll";

$descriptorspec = array(
   0 => array("pipe", "r"),   // stdin is a pipe that the child will read from
   1 => array("pipe", "w"),   // stdout is a pipe that the child will write to
   2 => array("pipe", "w")    // stderr is a pipe that the child will write to
);
flush();
$process = proc_open($cmd, $descriptorspec, $pipes);
echo 'opened'."\n";
if (is_resource($process)) {
    sleep(1);
    echo "blah\n";
    var_export(fgets($pipes[1]));
    echo "blah213\n";
    while ($s = fgets($pipes[1])) {
        print $s;
        flush();
    }
}

?>

I added the var_export to narrow down where the blocking was occurring. I get "opened" and "blah" output fine, but "blah123" doesn't come until I finish the enrollment process with the fingerprint scanner, at which point all the output from the fingerprint scanner comes along with it. The enroll.c app is pretty much the example enroll.c that comes with libfprint, with a few modifications to play sounds when a specific stage is successful. The "important" bit of that program is here:

struct fp_print_data *enroll(struct fp_dev *dev) {
    struct fp_print_data *enrolled_print = NULL;
    int r;

    do {
        struct fp_img *img = NULL;

        sleep(1);
        printf("\nScan your finger now.\n");

        r = fp_enroll_finger_img(dev, &enrolled_print, &img);
        printf("\nFinger scanned.\n");
        if (img) {
            fp_img_save_to_file(img, "enrolled.pgm");
            printf("Wrote scanned image to enrolled.pgm\n");
            fp_img_free(img);
        }
        if (r < 0) {
            printf("Enroll failed with error %d\n", r);
            play_error();
            return NULL;
        }

        switch (r) {
        case FP_ENROLL_COMPLETE:
            printf("Enroll complete!\n");
            break;
        case FP_ENROLL_FAIL:
            printf("Enroll failed, something wen't wrong :(\n");
            play_error();
            return NULL;
        case FP_ENROLL_PASS:
            printf("Enroll stage passed. Yay!\n");
            play_success();
            break;
        case FP_ENROLL_RETRY:
            printf("Didn't quite catch that. Please try again.\n");
            play_error();
            break;
        case FP_ENROLL_RETRY_TOO_SHORT:
            printf("Your swipe was too short, please try again.\n");
            play_error();
            break;
        case FP_ENROLL_RETRY_CENTER_FINGER:
            printf("Didn't catch that, please center your finger on the "
                "sensor and try again.\n");
            play_error();
            break;
        case FP_ENROLL_RETRY_REMOVE_FINGER:
            printf("Scan failed, please remove your finger and then try "
                "again.\n");
            play_error();
            break;
        }
    } while (r != FP_ENROLL_COMPLETE);

    if (!enrolled_print) {
        fprintf(stderr, "Enroll complete but no print?\n");
        return NULL;
    }

    printf("Enrollment completed!\n\n");
    play_success();
    return enrolled_print;
}

Here is the output of the PHP app once the C program terminates:

opened
blah 
-----NOTE: THE STUFF BELOW HERE DOESN'T DISPLAY UNTIL AFTER enroll TERMINATES-----
'Found device claimed by Digital Persona U.are.U 4000/4000B/4500 driver
'blah213
Opened device. It's now time to enroll your finger.


Scan your finger now.
uru4000:info [init_run_state] Versions 0040 and 0014

Finger scanned.
Wrote scanned image to enrolled.pgm
Enroll stage passed. Yay!

Scan your finger now.

Finger scanned.
Wrote scanned image to enrolled.pgm
Enroll stage passed. Yay!

Scan your finger now.

Finger scanned.
Wrote scanned image to enrolled.pgm
Enroll stage passed. Yay!

Scan your finger now.

Finger scanned.
Wrote scanned image to enrolled.pgm
Enroll stage passed. Yay!

Scan your finger now.

Finger scanned.
Wrote scanned image to enrolled.pgm
Enroll complete!
Enrollment completed!

Closing device

Any ideas as to why the fgets() call (have also tried fread() and stream_get_contents() with similar behavior) blocks until the C program terminates? I expect to get output as the program runs. If I switch to using $cmd = "ping 127.0.0.1";, it behaves as I would expect it to, outputting each line of the ping as it is output to stdout.

Update...fixed

Barmar was correct below, I just had to turn off output buffering....but in doing so, I encountered some weird inconsistencies that I wanted to document for anyone else having similar problems... The fix was

setbuf(stdout, NULL);

which according to this comment should be equivalent to

setvbuf(stdout, NULL, _IONBF, 0);

but with the setvbuf(stdout, NULL, _IONBF, 0);, the C program would output everything just fine, along with setvbuf(stdout, NULL, _IONBF, 0);. With setbuf(stdout, NULL);, everything worked perfectly.

What I find very interesting is that with a program like the following, the PHP script was able to get the stdio no problem, without turning output buffering off...and both scripts included stdio, so both should have had output buffering enabled...

#include <stdio.h>
#include <stdlib.h>
#include <alsa/asoundlib.h>
#include <alsa/pcm.h>

int main() {
  int val;

  printf("ALSA library version: %s\n", SND_LIB_VERSION_STR);

  printf("\nPCM stream types:\n");
  for (val = 0; val <= SND_PCM_STREAM_LAST; val++)
    printf("  %s\n",
      snd_pcm_stream_name((snd_pcm_stream_t)val));

  return 0;
}

Anyway, it's fixed now. Thanks!


Solution

  • Barmar was correct in what was causing the problem: stdio was being buffered. This is what fixed it for me:

    setbuf(stream, NULL);