Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Why system() returns 0 even though the program it executes dies

Tags:

perl

system

I'm trying to test a piece of code ($code) that should make sure that only one instance of the program is running at a time:

#!/usr/bin/perl
# test_lock
use strict;
use warnings;

( my $code = <<'CODE') =~ s/^\s+//gm;
    #!/usr/bin/perl
    use strict;
    use warnings;
    use Fcntl qw(:flock);

    # Make sure only one instance of the program is running at a time.
    open our $Lock, '<', $0 or die "Can't lock myself $0: $!";
    flock $Lock, LOCK_EX | LOCK_NB
      or die "Another instance of $0 is already running. Exiting ...\n";

    sleep(2);
CODE

my $progfile = '/tmp/x';
open my $fh, '>', $progfile or die $!;
print $fh $code;
close $fh;

$|++;
my $ex1 = system("perl $progfile &");
print "First system(): $ex1\n";
my $ex2 = system("perl $progfile");
print "Second system(): $ex2\n";

I expected that the second call to system() would return a non-zero value ($ex2) as it can't get the lock and dies. However I get:

$ perl test_lock
First system(): 0
Another instance of /tmp/x is already running. Exiting ...
Second system(): 0

What is wrong with my assumption? (Is there a better way to test the $code?)

like image 252
jreisinger Avatar asked Jan 08 '23 03:01

jreisinger


2 Answers

I think it likely because you have a race condition. How do you know that error is actually coming from your second process?

Because if you for example, run:

perl /tmp/x & perl /tmp/x ; echo $?

You may get a zero return, because the 'winner' of the race may well be the latter process (which return code you're catching). (Try it a few times, and you'll see different results)

You also do have slight difference is what the shell is doing between the two commands - from the docs:

If there is only one scalar argument, the argument is checked for shell metacharacters, and if there are any, the entire argument is passed to the system's command shell for parsing (this is /bin/sh -c on Unix platforms, but varies on other platforms). If there are no shell metacharacters in the argument, it is split into words and passed directly to execvp , which is more efficient.

So actually you should see invocation of sh before perl in your first, which means it's actually more likely to take longer to get to the lock point.

That means your command is more like:

sh -c "perl /tmp/x"& perl /tmp/x; echo $?

Run that a few times and see how many times you get non-zero error codes. It's not often, because usually the 'delay' of the shell start up is enough to ensure that the second instance wins the race most of the time!

If you've linux - try strace -fTt yourscript which will trace the execution flow. Or you can make judicious use of $$ to report the process-pid when running.

like image 144
Sobrique Avatar answered Mar 18 '23 01:03

Sobrique


In both cases, you are obtaining the exit code of the shell you launch. Roughly speaking, the shell returns the exit code of the last program it ran.

Since the shell created by system("perl $progfile &") doesn't wait for the child to end, it will virtually always return 0 since launching perl in the background is unlikely to result in an error.

So if the second instance of perl managed to obtain the lock first, you'll get the outcome you got. This race condition can be seem more clearly if you identify the source of the exception.

#!/usr/bin/perl
# test_lock
use strict;
use warnings;

( my $code = <<'CODE') =~ s/^\s+//gm;
    #!/usr/bin/perl
    use strict;
    use warnings;
    use Fcntl qw(:flock);

    # Make sure only one instance of the program is running at a time.
    open our $Lock, '<', $0 or die "Can't lock myself $0: $!";
    flock $Lock, LOCK_EX | LOCK_NB
      or die "$ARGV[0]: Another instance of $0 is already running. Exiting ...\n";

    sleep(2);
CODE

my $progfile = 'b.pl';
open my $fh, '>', $progfile or die $!;
print $fh $code;
close $fh;

$|++;
my $ex1 = system("perl $progfile 1 &");
print "First system(): $ex1\n";
my $ex2 = system("perl $progfile 2");
print "Second system(): $ex2\n";

Output:

$ perl a.pl
First system(): 0
1: Another instance of b.pl is already running. Exiting ...
Second system(): 0

$ perl a.pl
First system(): 0
2: Another instance of b.pl is already running. Exiting ...
Second system(): 2816
like image 43
ikegami Avatar answered Mar 18 '23 01:03

ikegami