]> git.proxmox.com Git - pve-common.git/blobdiff - src/PVE/Tools.pm
various perl critic fixes
[pve-common.git] / src / PVE / Tools.pm
index 460fa73947c4c084636e84c2bf6a9df68406f594..d933503d164a8a2d4abbf51c9c64f627b30fc519 100644 (file)
@@ -302,7 +302,7 @@ sub safe_read_from {
     my ($fh, $max, $oneline, $filename) = @_;
 
     # pmxcfs file size limit
-    $max = 512*1024 if !$max;
+    $max = 1024 * 1024 if !$max;
 
     my $subject = defined($filename) ? "file '$filename'" : 'input';
 
@@ -459,7 +459,7 @@ sub run_command {
            # to avoid that we open /dev/null
            if (!ref($writer) && !defined(fileno(STDIN))) {
                POSIX::close(0);
-               open(STDIN, "</dev/null");
+               open(STDIN, '<', '/dev/null');
            }
        };
 
@@ -484,7 +484,7 @@ sub run_command {
            close $writer;
        }
 
-       my $select = new IO::Select;
+       my $select = IO::Select->new();
        $select->add($reader) if ref($reader);
        $select->add($error);
 
@@ -1022,6 +1022,7 @@ sub run_fork_with_timeout {
     warn $@ if $@;
     $pipe_out->close();
     kill('KILL', $child);
+    # FIXME: hangs if $child doesn't exits?! (D state)
     waitpid($child, 0);
 
     alarm $prev_alarm;
@@ -1210,8 +1211,7 @@ sub decode_text {
     return Encode::decode("utf8", uri_unescape($data));
 }
 
-# depreciated - do not use!
-# we now decode all parameters by default
+# NOTE: deprecated - do not use! we now decode all parameters by default
 sub decode_utf8_parameters {
     my ($param) = @_;
 
@@ -1265,54 +1265,76 @@ sub split_args {
     return $str ? [ Text::ParseWords::shellwords($str) ] : [];
 }
 
-sub dump_logfile {
-    my ($filename, $start, $limit, $filter) = @_;
-
-    my $lines = [];
-    my $count = 0;
-
-    my $fh = IO::File->new($filename, "r");
-    if (!$fh) {
-       $count++;
-       push @$lines, { n => $count, t => "unable to open file - $!"};
-       return ($count, $lines);
-    }
+sub dump_logfile_by_filehandle {
+    my ($fh, $filter, $state) = @_;
 
-    $start = 0 if !$start;
-    $limit = 50 if !$limit;
+    my $count = ($state->{count} //= 0);
+    my $lines = ($state->{lines} //= []);
+    my $start = ($state->{start} //= 0);
+    my $limit = ($state->{limit} //= 50);
+    my $final = ($state->{final} //= 1);
+    my $read_until_end = ($state->{read_until_end} //= $limit == 0);
 
     my $line;
-
     if ($filter) {
        # duplicate code, so that we do not slow down normal path
        while (defined($line = <$fh>)) {
-           next if $line !~ m/$filter/;
+           if (ref($filter) eq 'CODE') {
+               next if !$filter->($line);
+           } else {
+               next if $line !~ m/$filter/;
+           }
            next if $count++ < $start;
-           next if $limit <= 0;
+           if (!$read_until_end) {
+               next if $limit <= 0;
+               $limit--;
+           }
            chomp $line;
            push @$lines, { n => $count, t => $line};
-           $limit--;
        }
     } else {
        while (defined($line = <$fh>)) {
            next if $count++ < $start;
-           next if $limit <= 0;
+           if (!$read_until_end) {
+               next if $limit <= 0;
+               $limit--;
+           }
            chomp $line;
            push @$lines, { n => $count, t => $line};
-           $limit--;
        }
     }
 
-    close($fh);
-
     # HACK: ExtJS store.guaranteeRange() does not like empty array
     # so we add a line
-    if (!$count) {
+    if (!$count && $final) {
        $count++;
        push @$lines, { n => $count, t => "no content"};
     }
 
-    return ($count, $lines);
+    $state->{count} = $count;
+    $state->{limit} = $limit;
+}
+
+sub dump_logfile {
+    my ($filename, $start, $limit, $filter) = @_;
+
+    my $fh = IO::File->new($filename, "r");
+    if (!$fh) {
+       return (1, { n => 1, t => "unable to open file - $!"});
+    }
+
+    my %state = (
+       'count' => 0,
+       'lines' => [],
+       'start' => $start,
+       'limit' => $limit,
+    );
+
+    dump_logfile_by_filehandle($fh, $filter, \%state);
+
+    close($fh);
+
+    return ($state{'count'}, $state{'lines'});
 }
 
 sub dump_journal {
@@ -1468,32 +1490,39 @@ sub parse_host_and_port {
 
 sub setresuid($$$) {
     my ($ruid, $euid, $suid) = @_;
-    return 0 == syscall(PVE::Syscall::setresuid, $ruid, $euid, $suid);
+    return 0 == syscall(PVE::Syscall::setresuid, int($ruid), int($euid), int($suid));
 }
 
 sub unshare($) {
     my ($flags) = @_;
-    return 0 == syscall(PVE::Syscall::unshare, $flags);
+    return 0 == syscall(PVE::Syscall::unshare, int($flags));
 }
 
 sub setns($$) {
     my ($fileno, $nstype) = @_;
-    return 0 == syscall(PVE::Syscall::setns, $fileno, $nstype);
+    return 0 == syscall(PVE::Syscall::setns, int($fileno), int($nstype));
 }
 
 sub syncfs($) {
     my ($fileno) = @_;
-    return 0 == syscall(PVE::Syscall::syncfs, $fileno);
+    return 0 == syscall(PVE::Syscall::syncfs, int($fileno));
 }
 
 sub fsync($) {
     my ($fileno) = @_;
-    return 0 == syscall(PVE::Syscall::fsync, $fileno);
+    return 0 == syscall(PVE::Syscall::fsync, int($fileno));
 }
 
 sub renameat2($$$$$) {
     my ($olddirfd, $oldpath, $newdirfd, $newpath, $flags) = @_;
-    return 0 == syscall(PVE::Syscall::renameat2, $olddirfd, $oldpath, $newdirfd, $newpath, $flags);
+    return 0 == syscall(
+       PVE::Syscall::renameat2,
+       int($olddirfd),
+       $oldpath,
+       int($newdirfd),
+       $newpath,
+       int($flags),
+    );
 }
 
 sub sync_mountpoint {
@@ -1507,6 +1536,12 @@ sub sync_mountpoint {
     die "syncfs '$path' failed - $syncfs_err\n" if defined $syncfs_err;
 }
 
+my sub check_mail_addr {
+    my ($addr) = @_;
+    die "'$addr' does not look like a valid email address or username\n"
+       if $addr !~ /^$EMAIL_RE$/ && $addr !~ /^$EMAIL_USER_RE$/;
+}
+
 # support sending multi-part mail messages with a text and or a HTML part
 # mailto may be a single email string or an array of receivers
 sub sendmail {
@@ -1514,88 +1549,90 @@ sub sendmail {
 
     $mailto = [ $mailto ] if !ref($mailto);
 
-    my $mailto_quoted = [];
-    for my $to (@$mailto) {
-       die "mailto does not look like a valid email address or username\n"
-           if $to !~ /^$EMAIL_RE$/ && $to !~ /^$EMAIL_USER_RE$/;
-       push @$mailto_quoted, shellquote($to);
-    }
-
-    my $rcvrtxt = join (', ', @$mailto);
+    check_mail_addr($_) for $mailto->@*;
+    my $to_quoted = [ map { shellquote($_) } $mailto->@* ];
 
     $mailfrom = $mailfrom || "root";
-    die "mailfrom does not look like a valid email address or username\n"
-           if $mailfrom !~ /^$EMAIL_RE$/ && $mailfrom !~ /^$EMAIL_USER_RE$/;
-    my $mailfrom_quoted = shellquote($mailfrom);
+    check_mail_addr($mailfrom);
+    my $from_quoted = shellquote($mailfrom);
 
     $author = $author // 'Proxmox VE';
 
-    open (MAIL, "|-", "sendmail", "-B", "8BITMIME", "-f", $mailfrom_quoted,
-       "--", @$mailto_quoted) || die "unable to open 'sendmail' - $!";
-
-    my $date = time2str('%a, %d %b %Y %H:%M:%S %z', time());
+    open (my $mail, "|-", "sendmail", "-B", "8BITMIME", "-f", $from_quoted, "--", $to_quoted->@*)
+       or die "unable to open 'sendmail' - $!";
 
     my $is_multipart = $text && $html;
+    my $boundary = "----_=_NextPart_001_" . int(time()) . $$; # multipart spec, see rfc 1521
 
-    # multipart spec see https://www.ietf.org/rfc/rfc1521.txt
-    my $boundary = "----_=_NextPart_001_".int(time).$$;
+    $subject = Encode::encode('MIME-Header', $subject) if $subject =~ /[^[:ascii:]]/;
 
-    if ($subject =~ /[^[:ascii:]]/) {
-       $subject = Encode::encode('MIME-Header', $subject);
-    }
+    print $mail "MIME-Version: 1.0\n" if $subject =~ /[^[:ascii:]]/ || $is_multipart;
 
-    if ($subject =~ /[^[:ascii:]]/ || $is_multipart) {
-       print MAIL "MIME-Version: 1.0\n";
-    }
-    print MAIL "From: $author <$mailfrom>\n";
-    print MAIL "To: $rcvrtxt\n";
-    print MAIL "Date: $date\n";
-    print MAIL "Subject: $subject\n";
+    print $mail "From: $author <$mailfrom>\n";
+    print $mail "To: " . join(', ', @$mailto) ."\n";
+    print $mail "Date: " . time2str('%a, %d %b %Y %H:%M:%S %z', time()) . "\n";
+    print $mail "Subject: $subject\n";
 
     if ($is_multipart) {
-       print MAIL "Content-Type: multipart/alternative;\n";
-       print MAIL "\tboundary=\"$boundary\"\n";
-       print MAIL "\n";
-       print MAIL "This is a multi-part message in MIME format.\n\n";
-       print MAIL "--$boundary\n";
+       print $mail "Content-Type: multipart/alternative;\n";
+       print $mail "\tboundary=\"$boundary\"\n";
+       print $mail "\n";
+       print $mail "This is a multi-part message in MIME format.\n\n";
+       print $mail "--$boundary\n";
     }
 
     if (defined($text)) {
-       print MAIL "Content-Type: text/plain;\n";
-       print MAIL "\tcharset=\"UTF-8\"\n";
-       print MAIL "Content-Transfer-Encoding: 8bit\n";
-       print MAIL "\n";
+       print $mail "Content-Type: text/plain;\n";
+       print $mail "\tcharset=\"UTF-8\"\n";
+       print $mail "Content-Transfer-Encoding: 8bit\n";
+       print $mail "\n";
 
        # avoid 'remove extra line breaks' issue (MS Outlook)
        my $fill = '  ';
        $text =~ s/^/$fill/gm;
 
-       print MAIL $text;
+       print $mail $text;
 
-       print MAIL "\n--$boundary\n" if $is_multipart;
+       print $mail "\n--$boundary\n" if $is_multipart;
     }
 
     if (defined($html)) {
-       print MAIL "Content-Type: text/html;\n";
-       print MAIL "\tcharset=\"UTF-8\"\n";
-       print MAIL "Content-Transfer-Encoding: 8bit\n";
-       print MAIL "\n";
+       print $mail "Content-Type: text/html;\n";
+       print $mail "\tcharset=\"UTF-8\"\n";
+       print $mail "Content-Transfer-Encoding: 8bit\n";
+       print $mail "\n";
 
-       print MAIL $html;
+       print $mail $html;
 
-       print MAIL "\n--$boundary--\n" if $is_multipart;
+       print $mail "\n--$boundary--\n" if $is_multipart;
     }
 
-    close(MAIL);
+    close($mail);
 }
 
+# creates a temporary file that does not shows up on the file system hierarchy.
+#
+# Uses O_TMPFILE if available, which makes it just an anon inode that never shows up in the FS.
+# If O_TMPFILE is not available, which unlikely nowadays (added in 3.11 kernel and all FS relevant
+# for us support it) back to open-create + immediate unlink while still holding the file  handle.
+#
+# TODO: to avoid FS dependent features we could (transparently) switch to memfd_create as backend
 sub tempfile {
     my ($perm, %opts) = @_;
 
     # default permissions are stricter than with file_set_contents
     $perm = 0600 if !defined($perm);
 
-    my $dir = $opts{dir} // '/run';
+    my $dir = $opts{dir};
+    if (!$dir) {
+       if (-d "/run/user/$<") {
+           $dir = "/run/user/$<";
+       } elsif ($< == 0) {
+           $dir = "/run";
+       } else {
+           $dir = "/tmp";
+       }
+    }
     my $mode = $opts{mode} // O_RDWR;
     $mode |= O_EXCL if !$opts{allow_links};
 
@@ -1610,6 +1647,7 @@ sub tempfile {
     return $fh;
 }
 
+# create an (ideally) anon file with the $data as content and return its FD-path and FH
 sub tempfile_contents {
     my ($data, $perm, %opts) = @_;
 
@@ -1643,7 +1681,11 @@ sub validate_ssh_public_keys {
 
 sub openat($$$;$) {
     my ($dirfd, $pathname, $flags, $mode) = @_;
-    my $fd = syscall(PVE::Syscall::openat, $dirfd, $pathname, $flags, $mode//0);
+    $dirfd = int($dirfd);
+    $flags = int($flags);
+    $mode = int($mode // 0);
+
+    my $fd = syscall(PVE::Syscall::openat, $dirfd, $pathname, $flags, $mode);
     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
@@ -1658,12 +1700,19 @@ sub openat($$$;$) {
 
 sub mkdirat($$$) {
     my ($dirfd, $name, $mode) = @_;
-    return syscall(PVE::Syscall::mkdirat, $dirfd, $name, $mode) == 0;
+    return syscall(PVE::Syscall::mkdirat, int($dirfd), $name, int($mode)) == 0;
 }
 
 sub fchownat($$$$$) {
     my ($dirfd, $pathname, $owner, $group, $flags) = @_;
-    return syscall(PVE::Syscall::fchownat, $dirfd, $pathname, $owner, $group, $flags) == 0;
+    return syscall(
+       PVE::Syscall::fchownat,
+       int($dirfd),
+       $pathname,
+       int($owner),
+       int($group),
+       int($flags),
+    ) == 0;
 }
 
 my $salt_starter = time();
@@ -1793,9 +1842,9 @@ sub open_tree($$$) {
     my ($dfd, $pathname, $flags) = @_;
     return PVE::Syscall::file_handle_result(syscall(
        &PVE::Syscall::open_tree,
-       $dfd,
+       int($dfd),
        $pathname,
-       $flags,
+       int($flags),
     ));
 }
 
@@ -1803,26 +1852,26 @@ sub move_mount($$$$$) {
     my ($from_dirfd, $from_pathname, $to_dirfd, $to_pathname, $flags) = @_;
     return 0 == syscall(
        &PVE::Syscall::move_mount,
-       $from_dirfd,
+       int($from_dirfd),
        $from_pathname,
-       $to_dirfd,
+       int($to_dirfd),
        $to_pathname,
-       $flags,
+       int($flags),
     );
 }
 
 sub fsopen($$) {
     my ($fsname, $flags) = @_;
-    return PVE::Syscall::file_handle_result(syscall(&PVE::Syscall::fsopen, $fsname, $flags));
+    return PVE::Syscall::file_handle_result(syscall(&PVE::Syscall::fsopen, $fsname, int($flags)));
 }
 
 sub fsmount($$$) {
     my ($fd, $flags, $mount_attrs) = @_;
     return PVE::Syscall::file_handle_result(syscall(
        &PVE::Syscall::fsmount,
-       $fd,
-       $flags,
-       $mount_attrs,
+       int($fd),
+       int($flags),
+       int($mount_attrs),
     ));
 }
 
@@ -1830,15 +1879,22 @@ sub fspick($$$) {
     my ($dirfd, $pathname, $flags) = @_;
     return PVE::Syscall::file_handle_result(syscall(
        &PVE::Syscall::fspick,
-       $dirfd,
+       int($dirfd),
        $pathname,
-       $flags,
+       int($flags),
     ));
 }
 
 sub fsconfig($$$$$) {
     my ($fd, $command, $key, $value, $aux) = @_;
-    return 0 == syscall(&PVE::Syscall::fsconfig, $fd, $command, $key, $value, $aux);
+    return 0 == syscall(
+       &PVE::Syscall::fsconfig,
+       int($fd),
+       int($command),
+       $key,
+       $value,
+       int($aux),
+    );
 }
 
 # "raw" mount, old api, not for generic use (as it does not invoke any helpers).
@@ -1850,11 +1906,57 @@ sub mount($$$$$) {
        $source,
        $target,
        $filesystemtype,
-       $mountflags,
+       int($mountflags),
        $data,
     );
 }
 
+# size is optional and defaults to 256, note that xattr limits are FS specific and that xattrs can
+# get arbitrary long. pass `0` for $size in array context to get the actual size of a value
+sub getxattr($$;$) {
+    my ($path_or_handle, $name, $size) = @_;
+    $size //= 256;
+    my $buf = pack("x${size}");
+
+    my $xattr_size = -1; # the actual size of the xattr, can be zero
+    if (defined(my $fd = fileno($path_or_handle))) {
+       $xattr_size = syscall(&PVE::Syscall::fgetxattr, $fd, $name, $buf, int($size));
+    } else {
+       $xattr_size = syscall(&PVE::Syscall::getxattr, $path_or_handle, $name, $buf, int($size));
+    }
+    if ($xattr_size < 0) {
+       return undef;
+    }
+    $buf = substr($buf, 0, $xattr_size);
+    return wantarray ? ($buf, $xattr_size) : $buf;
+}
+
+# NOTE: can take either a path or an open file handle, i.e., its multiplexing setxattr and fsetxattr
+sub setxattr($$$;$) {
+    my ($path_or_handle, $name, $value, $flags) = @_;
+    my $size = length($value); # NOTE: seems to get correct length also for wide-characters in text..
+
+    if (defined(my $fd = fileno($path_or_handle))) {
+       return 0 == syscall(
+           &PVE::Syscall::fsetxattr,
+           $fd,
+           $name,
+           $value,
+           int($size),
+           int($flags // 0),
+       );
+    } else {
+       return 0 == syscall(
+           &PVE::Syscall::setxattr,
+           $path_or_handle,
+           $name,
+           $value,
+           int($size),
+           int($flags // 0),
+       );
+    }
+}
+
 sub safe_compare {
     my ($left, $right, $cmp) = @_;