#!./perl # # Test inheriting file descriptors across exec (close-on-exec). # # perlvar describes $^F aka $SYSTEM_FD_MAX as follows: # # The maximum system file descriptor, ordinarily 2. System file # descriptors are passed to exec()ed processes, while higher file # descriptors are not. Also, during an open(), system file descriptors # are preserved even if the open() fails. (Ordinary file descriptors # are closed before the open() is attempted.) The close-on-exec # status of a file descriptor will be decided according to the value of # C<$^F> when the corresponding file, pipe, or socket was opened, not # the time of the exec(). # # This documented close-on-exec behaviour is typically implemented in # various places (e.g. pp_sys.c) with code something like: # # #if defined(HAS_FCNTL) && defined(F_SETFD) # fcntl(fd, F_SETFD, fd > PL_maxsysfd); /* ensure close-on-exec */ # #endif # # This behaviour, therefore, is only currently implemented for platforms # where: # # a) HAS_FCNTL and F_SETFD are both defined # b) Integer fds are native OS handles # # ... which is typically just the Unix-like platforms. # # Notice that though integer fds are supported by the C runtime library # on Windows, they are not native OS handles, and so are not inherited # across an exec (though native Windows file handles are). BEGIN { chdir 't' if -d 't'; @INC = '../lib'; use Config; if (!$Config{'d_fcntl'}) { print("1..0 # Skip: fcntl() is not available\n"); exit(0); } require './test.pl'; } use strict; $|=1; my $Is_VMS = $^O eq 'VMS'; my $Is_MacOS = $^O eq 'MacOS'; my $Is_Win32 = $^O eq 'MSWin32'; # When in doubt, skip. skip_all("MacOS") if $Is_MacOS; skip_all("VMS") if $Is_VMS; skip_all("Win32") if $Is_Win32; sub make_tmp_file { my ($fname, $fcontents) = @_; local *FHTMP; open FHTMP, ">$fname" or die "open '$fname': $!"; print FHTMP $fcontents or die "print '$fname': $!"; close FHTMP or die "close '$fname': $!"; } my $Perl = which_perl(); my $quote = $Is_VMS || $Is_Win32 ? '"' : "'"; my $tmperr = tempfile(); my $tmpfile1 = tempfile(); my $tmpfile2 = tempfile(); my $tmpfile1_contents = "tmpfile1 line 1\ntmpfile1 line 2\n"; my $tmpfile2_contents = "tmpfile2 line 1\ntmpfile2 line 2\n"; make_tmp_file($tmpfile1, $tmpfile1_contents); make_tmp_file($tmpfile2, $tmpfile2_contents); # $Child_prog is the program run by the child that inherits the fd. # Note: avoid using ' or " in $Child_prog since it is run with -e my $Child_prog = <<'CHILD_PROG'; my $fd = shift; print qq{childfd=$fd\n}; open INHERIT, qq{<&=$fd} or die qq{open $fd: $!}; my $line = ; close INHERIT or die qq{close $fd: $!}; print $line CHILD_PROG $Child_prog =~ tr/\n//d; plan(tests => 22); sub test_not_inherited { my $expected_fd = shift; ok( -f $tmpfile2, "tmpfile '$tmpfile2' exists" ); my $cmd = qq{$Perl -e $quote$Child_prog$quote $expected_fd}; # Expect 'Bad file descriptor' or similar to be written to STDERR. local *SAVERR; open SAVERR, ">&STDERR"; # save original STDERR open STDERR, ">$tmperr" or die "open '$tmperr': $!"; my $out = `$cmd`; my $rc = $? >> 8; open STDERR, ">&SAVERR" or die "error: restore STDERR: $!"; close SAVERR or die "error: close SAVERR: $!"; # XXX: it seems one cannot rely on a non-zero return code, # at least not on Tru64. # cmp_ok( $rc, '!=', 0, # "child return code=$rc (non-zero means cannot inherit fd=$expected_fd)" ); cmp_ok( $out =~ tr/\n//, '==', 1, "child stdout: has 1 newline (rc=$rc, should be non-zero)" ); is( $out, "childfd=$expected_fd\n", 'child stdout: fd' ); } sub test_inherited { my $expected_fd = shift; ok( -f $tmpfile1, "tmpfile '$tmpfile1' exists" ); my $cmd = qq{$Perl -e $quote$Child_prog$quote $expected_fd}; my $out = `$cmd`; my $rc = $? >> 8; cmp_ok( $rc, '==', 0, "child return code=$rc (zero means inherited fd=$expected_fd ok)" ); my @lines = split(/^/, $out); cmp_ok( $out =~ tr/\n//, '==', 2, 'child stdout: has 2 newlines' ); cmp_ok( scalar(@lines), '==', 2, 'child stdout: split into 2 lines' ); is( $lines[0], "childfd=$expected_fd\n", 'child stdout: fd' ); is( $lines[1], "tmpfile1 line 1\n", 'child stdout: line 1' ); } $^F == 2 or print STDERR "# warning: \$^F is $^F (not 2)\n"; # Should not be able to inherit > $^F in the default case. open FHPARENT2, "<$tmpfile2" or die "open '$tmpfile2': $!"; my $parentfd2 = fileno FHPARENT2; defined $parentfd2 or die "fileno: $!"; cmp_ok( $parentfd2, '>', $^F, "parent open fd=$parentfd2 (\$^F=$^F)" ); test_not_inherited($parentfd2); close FHPARENT2 or die "close '$tmpfile2': $!"; # Should be able to inherit $^F after setting to $parentfd2 # Need to set $^F before open because close-on-exec set at time of open. $^F = $parentfd2; open FHPARENT1, "<$tmpfile1" or die "open '$tmpfile1': $!"; my $parentfd1 = fileno FHPARENT1; defined $parentfd1 or die "fileno: $!"; cmp_ok( $parentfd1, '<=', $^F, "parent open fd=$parentfd1 (\$^F=$^F)" ); test_inherited($parentfd1); close FHPARENT1 or die "close '$tmpfile1': $!"; # ... and test that you cannot inherit fd = $^F+n. open FHPARENT1, "<$tmpfile1" or die "open '$tmpfile1': $!"; open FHPARENT2, "<$tmpfile2" or die "open '$tmpfile2': $!"; $parentfd2 = fileno FHPARENT2; defined $parentfd2 or die "fileno: $!"; cmp_ok( $parentfd2, '>', $^F, "parent open fd=$parentfd2 (\$^F=$^F)" ); test_not_inherited($parentfd2); close FHPARENT2 or die "close '$tmpfile2': $!"; close FHPARENT1 or die "close '$tmpfile1': $!"; # ... and now you can inherit after incrementing. $^F = $parentfd2; open FHPARENT2, "<$tmpfile2" or die "open '$tmpfile2': $!"; open FHPARENT1, "<$tmpfile1" or die "open '$tmpfile1': $!"; $parentfd1 = fileno FHPARENT1; defined $parentfd1 or die "fileno: $!"; cmp_ok( $parentfd1, '<=', $^F, "parent open fd=$parentfd1 (\$^F=$^F)" ); test_inherited($parentfd1); close FHPARENT1 or die "close '$tmpfile1': $!"; close FHPARENT2 or die "close '$tmpfile2': $!";