2

I created a child process via IPC::Open2.
I need to read from the stdout of this child process line by line.
Problem is, as the stdout of the child process is not connected to a terminal, it's fully buffered and I can't read from it until the process terminates.

How can I flush the output of the child process without modifying its code ?


child process code

while (<STDIN>) {
    print "Received : $_";
}

parent process code:

use IPC::Open2;
use Symbol;

my $in = gensym();
my $out = gensym();

my $pid = open2($out, $in, './child_process');

while (<STDIN>) {
    print $in $_;
    my $line = <$out>;
    print "child said : $line";
}

When I run the code, it get stucks waiting the output of the child process.
However, if I run it with bc the result is what I expect, I believe bc must manually flush its output

note:
In the child process if I add $| = 1 at the beginning or STDOUT->flush() after printing, the parent process can properly read from it.
However this is an example and I must handle programs that don't manually flush their output.

zdim
  • 64,580
  • 5
  • 52
  • 81
Yanis.F
  • 612
  • 6
  • 18
  • 1
    Some quick looking on metacpan suggests [IPC::Run](https://metacpan.org/pod/IPC::Run) might be worth looking into; it has a way to run a process in a pseudo tty which might avoid the buffering problem you're encountering. – Shawn Feb 05 '19 at 19:24

2 Answers2

3

Unfortunately Perl has no control over the buffering behavior of the programs it executes. Some systems have an unbuffer utility that can do this. If you have access to this tool, you could say

my $pid = open2($out, $in, 'unbuffer ./child_process');

There's a discussion here about the equivalent tools for Windows, but I couldn't say whether any of them are effective.

mob
  • 117,087
  • 18
  • 149
  • 283
2

One way to (try to) deal with buffering is to set up a terminal-like environment for the process, a pseudo-terminal (pty). That is not easy to do in general but IPC::Run has that capability ready for easy use.

Here is the driver, run for testing using at facility so that it has no controlling terminal (or run it via cron)

use warnings;
use strict;
use feature 'say';

use IPC::Run qw(run);
    
my @cmd = qw(./t_term.pl input arguments); 
    
run \@cmd, '>pty>', sub { say "out: @_" };

#run \@cmd, '>', sub { say "out: @_" }   # no pty

With >pty> it sets up a pseudo-terminal for STDOUT of the program in @cmd (with > it's a pipe); also see <pty< and see more about redirection. The anonymous sub {} gets called every time there is output from the child, so one can process it as it goes. There are other related options.

The program that is called (t_term.pl) only tests for a terminal

use warnings;
use strict;
use feature 'say';

say "Is STDOUT filehandle attached to a terminal: ",
    ( (-t STDOUT) ? "yes" : "no" );
sleep 2;
say "bye from $$";

The -t STDOUT (see filetest operators) is a suitable way to check for a terminal in this example. For more/other ways see this post.

The output shows that the called program (t_term.pl) does see a terminal on its STDOUT, even when a driver runs without one (using at, or out of a crontab). If the >pty> is changed to the usual redirection > (a pipe) then there is no terminal.

Whether this solves the buffering problem is clearly up to that program, and to whether it is enough to fool it with a terminal.

Another way around the problem is using unbuffer when possible, as in mob's answer.

zdim
  • 64,580
  • 5
  • 52
  • 81
  • If I want to `cd` to a specific location before doing this `run \@cmd, '>pty>', sub { say "out: @_" }`, how should I modify this command ? The file that I want to run cannot be run from other locations. – him Jun 14 '19 at 06:18
  • @him Do you mean `chdir`? That changes script's working directory. So if you do `chdir $dir` in the script then if there is a program (say, `prog`) in that directory you can run it without path (so just `./prog` or probably even `prog`). It's a direct equivalent of doing `cd dir` on the command line and then running `prog`. Is that what you mean? – zdim Jun 14 '19 at 06:57
  • I have a script to run another program, before running that command I want to change my current directory to some other location from inside the script just for running the program - the program can't be run from elsewhere. I tried to write something like `run \@cdCmd, '>pty>', sub { say "out: @_" }, '|', \@cmd, '>pty>', sub { say "out: @_" }` but it says `'cd' not found ....`. This may be because `cd` is a builitin shell command. So, I wanted to know a method to do this. – him Jun 14 '19 at 07:12
  • 1
    @him OK, so that's what I said -- add `chdir $dir;`to your script, and after that you can do `run \@cmd ...` (`$dir` is directory you need to be in, where that program is). The Perl builtin to change the working directory of the script is [chdir](https://perldoc.perl.org/functions/chdir.html), which you run as a separate command first. – zdim Jun 14 '19 at 07:19
  • @him Try this on the command line: `perl -MCwd -wE'$d=shift//".."; say "in: ", cwd; chdir $d; say "now in: ", cwd'`. The program prints the working directory, then changes it (by default just to one directory up, `..`), then it prints it again. You can give it the directory to go to: `perl -MCwd -wE'...' dir-to-go-to` – zdim Jun 14 '19 at 07:23
  • @him Once you do `chdir $dir;` don't forget that after that point your script runs in `$dir`! So if you need to change back to where you started it, record where you are (using `cwd` from `Cwd` module) before the change so that you can change back once you need that – zdim Jun 14 '19 at 07:25