0

I'm a newb' on Perl, and try to do a simple script's launcher in Perl with Curses (Curses::UI)

On Stackoverflow I found a solution to print (in Perl) in real time the output of a Bash script.

But I can't do this with my Curses script, to write this output in a TextEditor field.

For example, the Perl script :

#!/usr/bin/perl -w

use strict;
use Curses::UI;
use Curses::Widgets;
use IO::Select;

my $cui = new Curses::UI( -color_support => 1 );

[...]

my $process_tracking = $container_middle_right->add(
    "text", "TextEditor",
    -readonly       => 1,
    -text           =>  "",
);

sub launch_and_read()
{
    my $s = IO::Select->new();
    open my $fh, '-|', './test.sh';
    $s->add($fh);

    while (my @readers = $s->can_read()) {
        for my $fh (@readers) {
            if (eof $fh) {
                $s->remove($fh);
                next;
            }
            my $l = <$fh>;
            $process_tracking->text( $l );
            my $actual_text = $process_tracking->text() . "\n";
            my $new_text = $actual_text . $l;
            $process_tracking->text( $new_text );
            $process_tracking->cursor_to_end();
        }
    }
}

[...]

$cui->mainloop();

This script contains a button to launch launch_and_read().

And the test.sh :

#!/bin/bash
for i in $( seq 1 5 )
do
    sleep 1
    echo "from $$ : $( date )"
done

The result is my application freeze while the bash script is executed, and the final output is wrote on my TextEditor field at the end.

Is there a solution to show in real time what's happened in the Shell script, without blocking the Perl script ?

Many thanks, and sorry if this question seems to be stupid :x

1 Answers1

0

You can't block. Curses's loop needs to run to process events. So you must poll. select with a timeout of zero can be used to poll.

my $sel;

sub launch_child {
   $sel = IO::Select->new();
   open my $fh, '-|', './test.sh';
   $sel->add($fh);
}

sub read_from_child {
   if (my @readers = $sel->can_read(0)) {
      for my $fh (@readers) {
         my $rv = sysread($fh, my $buf, 64*1024);
         if (!$rv) {
            $sel->remove($fh);
            close($fh);
            next;
         }

         ... add contents of $buf to the ui here ...
      }
   }
}

launch_child();
$cui->set_timer(read_from_child => \&read_from_child, 1);
$cui->mainloop();

Untested.

Note that I switched from readline (<>) to sysread since the former blocks until a newline is received. Using blocking calls like read or readline defies the point of using select. Furthermore, using buffering calls like read or readline can cause select to say nothing is waiting when there actually is. Never use read and readline with select.

ikegami
  • 367,544
  • 15
  • 269
  • 518
  • I just changed `if ($sel->can_read(0)) {` by `if (my @readers = $sel->can_read(0)) {`, and it's almost work ; I have to do something (press a key, change list selection) to refresh the TextEditor. Curses doesn't refresh if I don't do anything ? – user2388767 May 16 '13 at 08:01
  • In addition to my previous comment : Curses seems to launch mainloop each time it receives an input ; now my problem is, how to run mainloop permanently without waiting any user input... – user2388767 May 16 '13 at 08:17
  • The problem is (roughly speaking) that it only calls callbacks after processing another event. Replaced the callback with a timer. That should solve that problem. It'll poll the child every second, the smallest period supported. – ikegami May 16 '13 at 08:24
  • Works like a charm :) I'll try, just to see if it can works, a solution like this : http://stackoverflow.com/questions/2931951/perl-cursesui#answer-4944590 (maybe more frequently than 1 seconds to update ^^) – user2388767 May 16 '13 at 08:32
  • I think you'd be better off using `package Curses::UI; use Time::HiRes qw( time );` before `use Curses::UI;`, and use `0.1` instead of `1`. – ikegami May 16 '13 at 09:05