#!/usr/bin/php4 -q
<?php
#
#   php-gtk-defect-reproduce.php, test program demonstrating a problem
#   with the implementation of pipe buffering on PHP and the
#   combination with PHP-GTK's interface to GTK's input_add function.
#
#   Copyright (C) 2003  James Cameron (quozl@us.netrek.org)
#
#   This program is free software; you can redistribute it and/or modify
#   it under the terms of the GNU General Public License as published by
#   the Free Software Foundation; either version 2 of the License, or
#   (at your option) any later version.
#
#   This program is distributed in the hope that it will be useful,
#   but WITHOUT ANY WARRANTY; without even the implied warranty of
#   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#   GNU General Public License for more details.
#
#   You should have received a copy of the GNU General Public License
#   along with this program; if not, write to the Free Software
#   Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA
#

#
#   Test procedure: 
#
#   1.  examine and approve the definition of $command below (see what
#       the program executes for you),
#
#   2.  start the program, 
#
#   3.  press the left most button, (the test will take about 30 seconds),
#
#   4.  examine the output for evidence of pipe buffering (for me this
#       shows up as a delay before the remainder of the "df" and "w"
#       command output appears).
#
#   Tested on: php-4.1.2, php-4.3.0, php-gtk-0.5.2

# load the php-gtk functions
dl("php_gtk.so");

# start a subprocess with output to the GtkText widget
function start($name, $method) {
    global $pipe_method, $text, $input, $begin, $prior, $button;

    # store the method chosen by test user
    $pipe_method = $method;

    # prevent further tests while this test is in progress
    $button[0]->set_sensitive(0);
    $button[1]->set_sensitive(0);

    # record the start time of this test
    $begin = getmicrotime();
    $prior = $begin;

    # start the output log
    $text->delete_text(0, -1);
    $text->insert(NULL, NULL, NULL, 
		  "////////          time since start of test\n");
    $text->insert(NULL, NULL, NULL, 
		  "         ///////  time since previous line\n");
    $text->insert(NULL, NULL, NULL, 
		  "000.000: 000.000: start: $method\n");

    # a context array for sending to the reader function
    $context = array();

    # the command stream to be executed by the shell process
    $command = '(set -x;sleep 5;ps ax;df;w;sleep 5;echo 1 one;sleep 5;echo 2 two;sleep 5;echo 3 three;sleep 5;echo 4 four;sleep 5;echo exit)';

    # for pipe method, process input is the command, output is to
    # pipe, and GTK+ is asked to watch the pipe stream for input.
    if ($pipe_method == 'pipe') {
	$context['pipe'] = popen($command.' 2>&1', 'r');
    }

    # for fifo method, process input is from pipe, output is to fifo,
    # and GTK+ is asked to watch the fifo stream for input.
    if ($pipe_method == 'fifo') {
	$fifo = '/tmp/fifo.'.posix_getpid();
	$context['fifo'] = $fifo;
	@posix_mkfifo($fifo, 0700);
	$context['pope'] = popen($command.' > '.$fifo.' 2>&1', "w");
	$context['pipe'] = fopen($fifo, 'r');
    }

    # ask GTK+ to watch the input stream, and call reader() when data
    # is ready to be read.
    $input = gtk::input_add($context['pipe'], GDK_INPUT_READ, 'reader', $context);

    # We suspect that this is what is happening: the GTK+ function
    # expects a file descriptor, and when the fd becomes readable
    # causes our reader function to be called, which issues an fgets()
    # which is buffered stream I/O from the file descriptor.  The
    # stream buffer is filled by a read() that is inaccessible to us,
    # and we can then call fgets() a number of times before we stall.
    # We can't afford to stall, as this is a GUI!  So we only call
    # once, and the stream buffer never empties until pclose() time,
    # when the reader() is called by GTK+ repeatedly until we've
    # closed the file descriptor.
    #
    # Conclusion: gtk::input_add not useful, except for sockets
    # 
    # The gtk::input_add implementation in PHP-GTK suffers from
    # standard I/O buffering, rendering it useless for files, named
    # pipes, and popen() pipes.
    # 
    # Although the handler for arriving data is called, it can only do
    # blocking I/O at the standard I/O level; with fgets().
    # 
    # What happens is that the marshal function is called by GTK+ once
    # the select() call indicates that the file descriptor is
    # readable.  The user program PHP handler will call fgets(), which
    # must read() to fill the buffer.  This read() may clear the
    # readability state of the file descriptor.  The handler will not
    # be called again until more data has arrived, despite the data
    # yet to be read in the standard I/O buffer used by fgets().
    # 
    # Possible solutions that I can see:
    # 
    # 1) PHP to implement a non-blocking fgets(),
    # 2) PHP to implement an fread() of pipes,
    # 3) PHP or PHP-GTK to set pipe FILE * to be line buffered,
    # 4) PHP-GTK to re-call the handler until stream buffer empty,
    #    (how to tell if a standard I/O stream buffer has data?)
    # 
    # Workarounds available to the programmer above PHP-GTK:
    # 
    # 5) use unix domain socket instead of popen(),
    # 6) use AF_INET TCP socket instead of popen().
    # 
    # Could I have a technical review of the above, please?
    #
}

# input handler for pipe, displays what process writes to the pipe
function reader($ignore, $ignore, $context) {
    global $pipe_method, $text, $input, $begin, $prior, $button;

    # record the time that this function was called by GTK+
    $now = getmicrotime();

    # if more than one second has passed, insert a couple of blank lines
    if ($now - $prior > 1) {
	$text->insert(NULL, NULL, NULL, "\n\n");
    }
    $prior = $now;

    # read one line from the pipe
    $line = fgets($context['pipe'], 1024);

    # display the text from the process, with elapsed and delta times
    $text->insert(NULL, NULL, NULL, 
		  sprintf("%03.3f: %03.3f: %s", $now-$begin, $now-$prior, 
			  $line));

    # if not yet end of file on pipe, return true, indicating that
    # GTK+ should continue to call the function.
    if (!feof($context['pipe'])) return TRUE;

    # ask GTK+ to remove the input stream watch
    gtk::input_remove($input);

    # end of file seen, close the pipe
    if ($pipe_method == 'pipe') {
	$status = pclose($context['pipe']);
    }

    if ($pipe_method == 'fifo') {
	$status = fclose($context['pipe']);
	unlink($context['fifo']);
	$status = pclose($context['pope']);
    }

    # report the stop
    $text->insert(NULL, NULL, NULL, "stop: status = $status\n");

    # enable the start buttons again
    $button[0]->set_sensitive(1);
    $button[1]->set_sensitive(1);

    # request GTK+ not to call this function again [redundant?] 
    return FALSE;
}

# get the current time in microseconds
# from php manual entry for microtime
function getmicrotime(){ 
    list($usec, $sec) = explode(" ",microtime()); 
    return ((float)$usec + (float)$sec); 
} 

# quit the main loop
function quit() { Gtk::main_quit(); }

# construction of test window (not part of the problem being reported)

# widget instantiation hierarchy
#
#       GtkWindow
#       |
#       +-- GtkVBox
#           |
#           +-- GtkScrolledWindow
#           |   |
#           |   +-- GtkText
#           |   
#           +-- GtkHBox
#               |
#               +-- GtkButton
#               |
#               +-- GtkButton
#               |
#               +-- GtkButton

$main = &new GtkWindow;
$main->connect('delete-event', 'quit');
$main->set_title('php-gtk-defect-reproduce');

$vbox = &new GtkVBox();
$vbox->set_border_width(4);
$vbox->set_spacing(10);
$main->add($vbox);
$vbox->show();

$scrolled = &new GtkScrolledWindow();
$vbox->add($scrolled);
$scrolled->set_policy(GTK_POLICY_AUTOMATIC, GTK_POLICY_AUTOMATIC);
$scrolled->show();

$text = &new GtkText;
$style = $text->style;
$style = $style->copy();
$font = gdk::font_load('-*-fixed-medium-r-normal--15-*-*-*-*-*-iso8859-*');
$test = '--------------------------------------------------------------------------------'; # 80 columns
$width = $font->width($test);
$height = $font->height($test);
$height = max($height*10, $width/2);
$scrolled->set_usize($width, $height);
$style->font = $font;
$text->set_style($style);
$scrolled->add($text);
$text->show();

$hbox = &new GtkHButtonBox();
$vbox->pack_end($hbox, FALSE, FALSE, 0);

$button[0] = &new GtkButton("\$pipe = popen(\$command, 'r');\n");
$hbox->add($button[0]);
$button[0]->connect('clicked', 'start', 'pipe');
$button[0]->show();

$button[1] = &new GtkButton("popen(\$command.' > '.\$fifo, 'w')\n\$pipe = fopen(\$fifo, 'r')");
$hbox->add($button[1]);
$button[1]->connect('clicked', 'start', 'fifo');
$button[1]->show();

$button[2] = &new GtkButton('Quit');
$hbox->add($button[2]);
$button[2]->connect('clicked', 'quit');
$button[2]->show();

$main->show_all();

# execute the GTK+ main loop
Gtk::main();

