]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/Tools.pm
run_fork_with_timeout: allow returning complex structures
[pve-common.git] / src / PVE / Tools.pm
index f642286ff61cd458a1fc303779ed0d71b1da2b08..3080b3ebebc047daec9d3c98fee5503fe197b84c 100644 (file)
@@ -4,7 +4,8 @@ use strict;
 use warnings;
 use POSIX qw(EINTR EEXIST EOPNOTSUPP);
 use IO::Socket::IP;
-use Socket qw(AF_INET AF_INET6 AI_ALL AI_V4MAPPED AI_CANONNAME SOCK_DGRAM);
+use Socket qw(AF_INET AF_INET6 AI_ALL AI_V4MAPPED AI_CANONNAME SOCK_DGRAM
+             IPPROTO_TCP);
 use IO::Select;
 use File::Basename;
 use File::Path qw(make_path);
@@ -19,12 +20,15 @@ use base 'Exporter';
 use URI::Escape;
 use Encode;
 use Digest::SHA;
+use JSON;
 use Text::ParseWords;
 use String::ShellQuote;
 use Time::HiRes qw(usleep gettimeofday tv_interval alarm);
 use Net::DBus qw(dbus_uint32 dbus_uint64);
 use Net::DBus::Callback;
 use Net::DBus::Reactor;
+use Scalar::Util 'weaken';
+use PVE::Syscall;
 
 # avoid warning when parsing long hex values with hex()
 no warnings 'portable'; # Support for 64-bit ints required
@@ -122,7 +126,12 @@ sub run_with_timeout {
 }
 
 # flock: we use one file handle per process, so lock file
-# can be called multiple times and succeeds for the same process.
+# can be nested multiple times and succeeds for the same process.
+#
+# Since this is the only way we lock now and we don't have the old
+# 'lock(); code(); unlock();' pattern anymore we do not actually need to
+# count how deep we're nesting. Therefore this hash now stores a weak reference
+# to a boolean telling us whether we already have a lock.
 
 my $lock_handles =  {};
 
@@ -133,58 +142,67 @@ sub lock_file_full {
 
     my $mode = $shared ? LOCK_SH : LOCK_EX;
 
-    my $lock_func = sub {
-        if (!$lock_handles->{$$}->{$filename}) {
-           my $fh = new IO::File(">>$filename") ||
-               die "can't open file - $!\n";
-           $lock_handles->{$$}->{$filename} = { fh => $fh, refcount => 0};
-        }
+    my $lockhash = ($lock_handles->{$$} //= {});
+
+    # Returns a locked file handle.
+    my $get_locked_file = sub {
+       my $fh = IO::File->new(">>$filename")
+           or die "can't open file - $!\n";
 
-        if (!flock($lock_handles->{$$}->{$filename}->{fh}, $mode|LOCK_NB)) {
-            print STDERR "trying to acquire lock...";
+       if (!flock($fh, $mode|LOCK_NB)) {
+           print STDERR "trying to acquire lock...";
            my $success;
            while(1) {
-               $success = flock($lock_handles->{$$}->{$filename}->{fh}, $mode);
+               $success = flock($fh, $mode);
                # try again on EINTR (see bug #273)
                if ($success || ($! != EINTR)) {
                    last;
                }
            }
-            if (!$success) {
-                print STDERR " failed\n";
-                die "can't acquire lock '$filename' - $!\n";
-            }
-            print STDERR " OK\n";
-        }
-       $lock_handles->{$$}->{$filename}->{refcount}++;
+           if (!$success) {
+               print STDERR " failed\n";
+               die "can't acquire lock '$filename' - $!\n";
+           }
+           print STDERR " OK\n";
+       }
+
+       return $fh;
     };
 
     my $res;
-
-    eval { run_with_timeout($timeout, $lock_func); };
-    my $err = $@;
-    if ($err) {
-       $err = "can't lock file '$filename' - $err";
-    } else {
-       eval { $res = &$code(@param) };
-       $err = $@;
-    }
-
-    if (my $fh = $lock_handles->{$$}->{$filename}->{fh}) {
-       my $refcount = --$lock_handles->{$$}->{$filename}->{refcount};
-       if ($refcount <= 0) {
-           $lock_handles->{$$}->{$filename} = undef;
-           close ($fh);
+    my $checkptr = $lockhash->{$filename};
+    my $check = 0; # This must not go out of scope before running the code.
+    my $local_fh; # This must stay local
+    if (!$checkptr || !$$checkptr) {
+       # We cannot create a weak reference in a single atomic step, so we first
+       # create a false-value, then create a reference to it, then weaken it,
+       # and after successfully locking the file we change the boolean value.
+       #
+       # The reason for this is that if an outer SIGALRM throws an exception
+       # between creating the reference and weakening it, a subsequent call to
+       # lock_file_full() will see a leftover full reference to a valid
+       # variable. This variable must be 0 in order for said call to attempt to
+       # lock the file anew.
+       #
+       # An externally triggered exception elsewhere in the code will cause the
+       # weak reference to become 'undef', and since the file handle is only
+       # stored in the local scope in $local_fh, the file will be closed by
+       # perl's cleanup routines as well.
+       #
+       # This still assumes that an IO::File handle can properly deal with such
+       # exceptions thrown during its own destruction, but that's up to perls
+       # guts now.
+       $lockhash->{$filename} = \$check;
+       weaken $lockhash->{$filename};
+       $local_fh = eval { run_with_timeout($timeout, $get_locked_file) };
+       if ($@) {
+           $@ = "can't lock file '$filename' - $@";
+           return undef;
        }
+       $check = 1;
     }
-
-    if ($err) {
-        $@ = $err;
-        return undef;
-    }
-
-    $@ = undef;
-
+    $res = eval { &$code(@param); };
+    return undef if $@;
     return $res;
 }
 
@@ -341,7 +359,7 @@ sub run_command {
     my $timeout;
     my $oldtimeout;
     my $pid;
-    my $exitcode;
+    my $exitcode = -1;
 
     my $outfunc;
     my $errfunc;
@@ -563,6 +581,64 @@ sub run_command {
     return $exitcode;
 }
 
+# Run a command with a tcp socket as standard input.
+sub pipe_socket_to_command  {
+    my ($cmd, $ip, $port) = @_;
+
+    my $params = {
+       Listen => 1,
+       ReuseAddr => 1,
+       Proto => &Socket::IPPROTO_TCP,
+       GetAddrInfoFlags => 0,
+       LocalAddr => $ip,
+       LocalPort => $port,
+    };
+    my $socket = IO::Socket::IP->new(%$params) or die "failed to open socket: $!\n";
+
+    print "$ip\n$port\n"; # tell remote where to connect
+    *STDOUT->flush();
+
+    alarm 0;
+    local $SIG{ALRM} = sub { die "timed out waiting for client\n" };
+    alarm 30;
+    my $client = $socket->accept; # Wait for a client
+    alarm 0;
+    close($socket);
+
+    # We want that the command talks over the TCP socket and takes
+    # ownership of it, so that when it closes it the connection is
+    # terminated, so we need to be able to close the socket. So we
+    # can't really use PVE::Tools::run_command().
+    my $pid = fork() // die "fork failed: $!\n";
+    if (!$pid) {
+       POSIX::dup2(fileno($client), 0);
+       POSIX::dup2(fileno($client), 1);
+       close($client);
+       exec {$cmd->[0]} @$cmd or do {
+           warn "exec failed: $!\n";
+           POSIX::_exit(1);
+       };
+    }
+
+    close($client);
+    if (waitpid($pid, 0) != $pid) {
+       kill(15 => $pid); # if we got interrupted terminate the child
+       my $count = 0;
+       while (waitpid($pid, POSIX::WNOHANG) != $pid) {
+           usleep(100000);
+           $count++;
+           kill(9 => $pid), last if $count > 300; # 30 second timeout
+       }
+    }
+    if (my $sig = ($? & 127)) {
+       die "got signal $sig\n";
+    } elsif (my $exitcode = ($? >> 8)) {
+       die "exit code $exitcode\n";
+    }
+
+    return undef;
+}
+
 sub split_list {
     my $listtxt = shift || '';
 
@@ -722,18 +798,16 @@ sub wait_for_vnc_port {
     my $starttime = [gettimeofday];
     my $elapsed;
 
+    my $found;
     while (($elapsed = tv_interval($starttime)) < $timeout) {
-       if (my $fh = IO::File->new ("/proc/net/tcp", "r")) {
-           while (defined (my $line = <$fh>)) {
-               if ($line =~ m/^\s*\d+:\s+([0-9A-Fa-f]{8}):([0-9A-Fa-f]{4})\s/) {
-                   if ($port == hex($2)) {
-                       close($fh);
-                       return 1;
-                   }
-               }
+       # -Htln = don't print header, tcp, listening sockets only, numeric ports
+       run_command(['/bin/ss', '-Htln', "sport = :$port"], outfunc => sub {
+           my $line = shift;
+           if ($line =~ m/^LISTEN\s+\d+\s+\d+\s+\S+:(\d+)\s/) {
+               $found = 1 if ($port == $1);
            }
-           close($fh);
-       }
+       });
+       return 1 if $found;
        $sleeptime += 100000 if  $sleeptime < 1000000;
        usleep($sleeptime);
     }
@@ -742,7 +816,7 @@ sub wait_for_vnc_port {
 }
 
 sub next_unused_port {
-    my ($range_start, $range_end, $family) = @_;
+    my ($range_start, $range_end, $family, $address) = @_;
 
     # We use a file to register allocated ports.
     # Those registrations expires after $expiretime.
@@ -770,16 +844,18 @@ sub next_unused_port {
        }
 
        my $newport;
+       my %sockargs = (Listen => 5,
+                       ReuseAddr => 1,
+                       Family    => $family,
+                       Proto     => IPPROTO_TCP,
+                       GetAddrInfoFlags => 0);
+       $sockargs{LocalAddr} = $address if defined($address);
 
        for (my $p = $range_start; $p < $range_end; $p++) {
            next if $ports->{$p}; # reserved
 
-           my $sock = IO::Socket::IP->new(Listen => 5,
-                                          LocalPort => $p,
-                                          ReuseAddr => 1,
-                                          Family    => $family,
-                                          Proto     => 0,
-                                          GetAddrInfoFlags => 0);
+           $sockargs{LocalPort} = $p;
+           my $sock = IO::Socket::IP->new(%sockargs);
 
            if ($sock) {
                close($sock);
@@ -808,18 +884,79 @@ sub next_unused_port {
 }
 
 sub next_migrate_port {
-    my ($family) = @_;
-    return next_unused_port(60000, 60050, $family);
+    my ($family, $address) = @_;
+    return next_unused_port(60000, 60050, $family, $address);
 }
 
 sub next_vnc_port {
-    my ($family) = @_;
-    return next_unused_port(5900, 6000, $family);
+    my ($family, $address) = @_;
+    return next_unused_port(5900, 6000, $family, $address);
 }
 
 sub next_spice_port {
-    my ($family) = @_;
-    return next_unused_port(61000, 61099, $family);
+    my ($family, $address) = @_;
+    return next_unused_port(61000, 61099, $family, $address);
+}
+
+# sigkill after $timeout  a $sub running in a fork if it can't write a pipe
+# the $sub has to return a single scalar
+sub run_fork_with_timeout {
+    my ($timeout, $sub) = @_;
+
+    my $res;
+    my $error;
+    my $pipe_out = IO::Pipe->new();
+
+    # disable pending alarms, save their remaining time
+    my $prev_alarm = alarm 0;
+
+    # avoid leaving a zombie if the parent gets interrupted
+    my $sig_received;
+    local $SIG{INT} = sub { $sig_received++; };
+
+    my $child = fork();
+    if (!defined($child)) {
+       die "fork failed: $!\n";
+       return $res;
+    }
+
+    if (!$child) {
+       $pipe_out->writer();
+
+       eval {
+           $res = $sub->();
+           print {$pipe_out} encode_json({ result => $res });
+           $pipe_out->flush();
+       };
+       if (my $err = $@) {
+           print {$pipe_out} encode_json({ error => $err });
+           $pipe_out->flush();
+           POSIX::_exit(1);
+       }
+       POSIX::_exit(0);
+    }
+
+    $pipe_out->reader();
+
+    my $readvalues = sub {
+       local $/ = undef;
+       my $child_res = decode_json(scalar<$pipe_out>);
+       $res = $child_res->{result};
+       $error = $child_res->{error};
+    };
+    eval {
+       run_with_timeout($timeout, $readvalues);
+    };
+    warn $@ if $@;
+    $pipe_out->close();
+    kill('KILL', $child);
+    waitpid($child, 0);
+
+    alarm $prev_alarm;
+    die "interrupted by unexpected signal\n" if $sig_received;
+
+    die $error if $error;
+    return $res;
 }
 
 # NOTE: NFS syscall can't be interrupted, so alarm does
@@ -977,6 +1114,8 @@ sub decode_text {
     return Encode::decode("utf8", uri_unescape($data));
 }
 
+# depreciated - do not use!
+# we now decode all parameters by default
 sub decode_utf8_parameters {
     my ($param) = @_;
 
@@ -1081,7 +1220,7 @@ sub dump_logfile {
 }
 
 sub dump_journal {
-    my ($start, $limit, $since, $until) = @_;
+    my ($start, $limit, $since, $until, $service) = @_;
 
     my $lines = [];
     my $count = 0;
@@ -1100,6 +1239,7 @@ sub dump_journal {
 
     my $cmd = ['journalctl', '-o', 'short', '--no-pager'];
 
+    push @$cmd, '--unit', $service if $service;
     push @$cmd, '--since', $since if $since;
     push @$cmd, '--until', $until if $until;
     run_command($cmd, outfunc => $parser);
@@ -1230,17 +1370,17 @@ sub parse_host_and_port {
 
 sub unshare($) {
     my ($flags) = @_;
-    return 0 == syscall(272, $flags);
+    return 0 == syscall(PVE::Syscall::unshare, $flags);
 }
 
 sub setns($$) {
     my ($fileno, $nstype) = @_;
-    return 0 == syscall(308, $fileno, $nstype);
+    return 0 == syscall(PVE::Syscall::setns, $fileno, $nstype);
 }
 
 sub syncfs($) {
     my ($fileno) = @_;
-    return 0 == syscall(306, $fileno);
+    return 0 == syscall(PVE::Syscall::syncfs, $fileno);
 }
 
 sub sync_mountpoint {
@@ -1372,7 +1512,7 @@ sub validate_ssh_public_keys {
 
 sub openat($$$;$) {
     my ($dirfd, $pathname, $flags, $mode) = @_;
-    my $fd = syscall(257, $dirfd, $pathname, $flags, $mode//0);
+    my $fd = syscall(PVE::Syscall::openat, $dirfd, $pathname, $flags, $mode//0);
     return undef if $fd < 0;
     # sysopen() doesn't deal with numeric file descriptors apparently
     # so we need to convert to a mode string for IO::Handle->new_from_fd
@@ -1380,14 +1520,14 @@ sub openat($$$;$) {
     my $handle = IO::Handle->new_from_fd($fd, $flagstr);
     return $handle if $handle;
     my $err = $!; # save error before closing the raw fd
-    syscall(3, $fd); # close
+    syscall(PVE::Syscall::close, $fd); # close
     $! = $err;
     return undef;
 }
 
 sub mkdirat($$$) {
     my ($dirfd, $name, $mode) = @_;
-    return syscall(258, $dirfd, $name, $mode) == 0;
+    return syscall(PVE::Syscall::mkdirat, $dirfd, $name, $mode) == 0;
 }
 
 # NOTE: This calls the dbus main loop and must not be used when another dbus
@@ -1475,4 +1615,37 @@ sub encrypt_pw {
     return crypt(encode("utf8", $pw), "\$5\$$salt\$");
 }
 
+# intended usage: convert_size($val, "kb" => "gb")
+# on reduction (converting to a bigger unit) we round up by default if
+# information got lost. E.g. `convert_size(1023, "b" => "kb")` returns 1
+# use $no_round_up to switch this off, above example would then return 0
+sub convert_size {
+    my ($value, $from, $to, $no_round_up) = @_;
+
+    my $units = {
+       b  => 0,
+       kb => 1,
+       mb => 2,
+       gb => 3,
+       tb => 4,
+       pb => 5,
+    };
+
+    $from = lc($from); $to = lc($to);
+    die "unknown 'from' and/or 'to' units ($from => $to)"
+       if !(defined($units->{$from}) && defined($units->{$to}));
+
+    my $shift_amount = $units->{$from} - $units->{$to};
+
+    if ($shift_amount > 0) {
+       $value <<= ($shift_amount * 10);
+    } elsif ($shift_amount < 0) {
+       my $remainder = ($value & (1 << abs($shift_amount)*10) - 1);
+       $value >>= abs($shift_amount) * 10;
+       $value++ if $remainder && !$no_round_up;
+    }
+
+    return $value;
+}
+
 1;