Detecting error using Perl system() with multiple piped processes

Viewed 88

I'm trying to implement a MySQL database restore from a GPG-encrypted file.

The following works perfectly well:

my $status = system(
    "gpg --pinentry-mode loopback --passphrase $passphrase --decrypt $my_encrypted_backup_file"
  . " | "
  . "mysql --host=myhost --user=myuser --password=mysecret mydatabase"
);

assuming no error conditions.

However, if an error condition occurs during the first process (such as an incorrect $passphrase), $status == 0 which erroneously indicates success. I understand this is because the status is returned from the second process, the mysql process.

Is there a generalized way, using system(), to either obtain the status from all the piped-together processes, or to somehow detect an error if any one such process fails?

BTW, I have tested gpg by itself (without its output being piped into mysql) and it does return an error code when an incorrect $passphrase is entered.

A workaround might be some option flag in mysql that returns an error when it receives nothing from gpg. Another workaround is to break up the processes and use a tmp file of some sort. However, I'd love a more generalized solution. Thanks!

2 Answers

If you need fine control like that, don't use the shell.

Calls to mysql can be replaced with using the DBI and DBD::mysql libraries. gpg can be replaced with Crypt::GPG.

If this is not possible, do the piping yourself with open and its |- and -| modes.

open(
    my $gpg_out,
    "-|",
    "gpg --pinentry-mode loopback --passphrase $passphrase --decrypt $my_encrypted_backup_file"
) or die "Can't run gpg: $!";

open(
    my $mysql_in,
    "|-",
    "mysql --host=myhost --user=myuser --password=mysecret mydatabase"
) or die "Can't run mysql: $!";

while(my $line = <$gpg_out>) {
    print $mysql_in $line;
}

close $gpg_out;
close $mysql_in;

Thanks to @Schwern for suggesting IPC::Run. Here is a working and tested solution:

use IPC::Run qw( run );

my $gpg = [
  "gpg",
  "--pinentry-mode=loopback",
  "--passphrase=$my_passphrase",
  "--decrypt",
  $my_backupfilepath
];

my $mysql = [
  "mysql",
  "--host=$mysql_host"
  "--user=$mysql_user"
  "--password=$mysql_pass"
  $mysql_dbname
];

run( $gpg, '|', $mysql ) || die "Error";

I still can't manage to capture a detailed error message, and I'm still seeing overly chatty output from gpg & mysql.... but alas I've spent enough time battling Perl and GPG already! Improvements gladly accepted.

Unrelated to the core question, but for anyone using this recipe as is... to get GPG 2.1+ to accept a passphrase via commandline, and to not cache it, you must add the following to gpg-agent.conf:

allow-loopback-pinentry
default-cache-ttl 1
max-cache-ttl 1

Source: https://wiki.archlinux.org/index.php/GnuPG#pinentry

Related