Search code examples
perlpipeeofperl-ipc-run

How can I detect end of file on a pipe in a Perl script?


In a Perl script, I am running another process (openssl) and communicating with it via pipes. I run openssl with IPC::Run::start.

$openssl_stdin_handle = Symbol::gensym();
$openssl_stdout_handle = Symbol::gensym();
$openssl_stderr_handle = Symbol::gensym();
$harness = IPC::Run::start(\@openssl_command_words, '<pipe', $openssl_stdin_handle, '>pipe', $openssl_stdout_handle, '2>pipe', $openssl_stderr_handle);
my $io_select_for_stdin = IO::Select->new($openssl_stdin_handle);
my $io_select_for_stdout = IO::Select->new($openssl_stdout_handle);
my $io_select_for_stderr = IO::Select->new($openssl_stderr_handle);

I write plaintext data to openssl with $io_select_for_stdin->can_write(4) and syswrite. I read encrypted data from openssl with $io_select_for_stdout->can_read($timeout) and sysread. I can't write all the plaintext data and then read all the encrypted data because, if there is a lot of plaintext data, the pipe from openssl's standard output fills up and openssl hangs. Instead, I have to write some plaintext, see if there is any encrypted data to read, read it if available, then go back and write some more.

Once I'm finished writing plaintext to openssl, I close $openssl_stdin_handle and call $io_select_for_stdout->can_read(1). Here is the code (it is within a loop labeled READ_WRITE_LOOP):

$OS_ERROR = 0
if  (not $io_select_for_stdout->can_read(1))
    {
    if  ($OS_ERROR == 0) {last READ_WRITE_LOOP;}   # nothing to read -- not an error
    return "error";
    }

my $number_of_cyphertext_bytes_read = sysread($openssl_stdout_handle, $cyphertext_block, $block_size);

The problem is that can_read could return false because openssl is not ready to write, or because openssl has finished writing and has closed its standard output. In the first case, I want to retry the read, in the second case I want to close $openssl_stdout_handle and move on to the next phase of the program. How do I distinguish between these two cases?

The code above is working because I have a timeout set to one second for can_read. If I shorten the timeout to .01 seconds, the code works sometimes and not others. I'd like a shorter timeout but I need the code to work every time.

I know that calling eof($openssl_stdout_handle) is not the answer. You can't mix calls to sysread and eof. Is there a clean way to detect whether openssl has closed its side of the pipe that I access via $openssl_stdout_handle?


Solution

  • To answer the title question, sysread returns zero on EOF (not to be confused with undef which is returned on error), pipe or not.


    As for the actual question, it seems to me you could use the following:

    run \@cmd, '<', \$unencrypted, '>', \my $encrypted;
    

    If you don't want to hold the entire file in memory, you can use a callback (sub { }) instead of a pipe for the output.

    run \@cmd,
       '<', sub {
          # Called repeatedly until it returns `undef`.
          # Return a string of bytes to send, or `undef` to signal eof...
       },
       '>', sub {
          # Do something with the string of bytes provided as argument...
       };