+#!/usr/bin/perl
+
+use lib '../src';
+use strict;
+use warnings;
+
+use Socket;
+use POSIX (); # don't import assert()
+
+use PVE::Tools 'lock_file_full';
+
+my $name = "test.lockfile.$$-";
+
+END {
+ system("rm $name*");
+};
+
+# Utilities:
+
+sub forked($$) {
+ my ($code1, $code2) = @_;
+
+ pipe(my $except_r, my $except_w) or die "pipe: $!\n";
+
+ my $pid = fork();
+ die "fork failed: $!\n" if !defined($pid);
+
+ if ($pid == 0) {
+ close($except_r);
+ eval { $code1->() };
+ if ($@) {
+ print {$except_w} $@;
+ $except_w->flush();
+ POSIX::_exit(1);
+ }
+ POSIX::_exit(0);
+ }
+ close($except_w);
+
+ eval { $code2->() };
+ my $err = $@;
+ if ($err) {
+ kill(15, $pid);
+ } else {
+ my $err = do { local $/ = undef; <$except_r> };
+ }
+ die "interrupted\n" if waitpid($pid, 0) != $pid;
+ die $err if $err;
+
+ # Check exit code:
+ my $status = POSIX::WEXITSTATUS($?);
+ if ($? == -1) {
+ die "failed to execute\n";
+ } elsif (POSIX::WIFSIGNALED($?)) {
+ my $sig = POSIX::WTERMSIG($?);
+ die "got signal $sig\n";
+ } elsif ($status != 0) {
+ die "exit code $status\n";
+ }
+}
+
+# Book-keeping:
+
+my %_ran;
+sub new {
+ %_ran = ();
+}
+sub ran {
+ my ($what) = @_;
+ $_ran{$what} = 1;
+}
+sub assert {
+ my ($what) = @_;
+ die "code didn't run: $what\n" if !$_ran{$what};
+}
+sub assert_not {
+ my ($what) = @_;
+ die "code shouldn't have run: $what\n" if $_ran{$what};
+}
+
+# Regular lock:
+new();
+lock_file_full($name, 10, 0, sub { ran('single lock') });
+assert('single lock');
+
+# Lock multiple times in a row:
+new();
+lock_file_full($name, 10, 0, sub { ran('lock A') });
+assert('lock A');
+lock_file_full($name, 10, 0, sub { ran('lock B') });
+assert('lock B');
+
+# Nested lock:
+new();
+lock_file_full($name, 10, 0, sub {
+ ran('lock A');
+ lock_file_full($name, 10, 0, sub { ran('lock B') });
+ assert('lock B');
+ ran('lock C');
+});
+assert('lock A');
+assert('lock B');
+assert('lock C');
+
+# Independent locks:
+new();
+lock_file_full($name, 10, 0, sub {
+ ran('lock A');
+ # locks file "${name}2"
+ lock_file_full($name.2, 10, 0, sub { ran('lock B') });
+ assert('lock B');
+ ran('lock C');
+});
+assert('lock A');
+assert('lock B');
+assert('lock C');
+
+# Does it actually lock? (shared=0)
+# Can we get two simultaneous shared locks? (shared=1)
+sub forktest1($) {
+ my ($shared) = @_;
+ new();
+ # socket pair for synchronization
+ socketpair(my $fmain, my $fother, AF_UNIX, SOCK_STREAM, PF_UNSPEC)
+ or die "socketpair(): $!\n";
+ forked sub {
+ # other side
+ close($fmain);
+ my $line;
+ lock_file_full($name, 60, $shared, sub {
+ ran('other side');
+ # tell parent we've acquired the lock
+ print {$fother} "1\n";
+ $fother->flush();
+ # wait for parent to be done trying to lock
+ $line = <$fother>;
+ });
+ die $@ if $@;
+ die "parent failed\n" if !$line || $line ne "2\n";
+ assert('other side');
+ }, sub {
+ # main process
+ # Wait for our child to lock:
+ close($fother);
+ my $line = <$fmain>;
+ die "child failed to acquire a lock\n" if !$line || $line ne "1\n";
+ lock_file_full($name, 1, $shared, sub {
+ ran('local side');
+ });
+ if ($shared) {
+ assert('local side');
+ } else {
+ assert_not('local side');
+ }
+ print {$fmain} "2\n";
+ $fmain->flush();
+ };
+ close($fmain);
+}
+forktest1(0);
+forktest1(1);
+print "Ok\n"; # Line-terminate the 'trying to acquire lock' message(s)