275 lines
7.5 KiB
Perl
275 lines
7.5 KiB
Perl
package PVE::PVEMod::Utils;
|
|
|
|
use strict;
|
|
use warnings;
|
|
use Exporter 'import';
|
|
|
|
use JSON;
|
|
use Fcntl qw(O_CREAT O_EXCL O_WRONLY);
|
|
|
|
use PVE::PVEMod::Config qw($DEBUG_ENABLED $VERSION $pve_mod_working_dir %config);
|
|
|
|
my $debug_log_fh;
|
|
|
|
our @EXPORT_OK = qw(
|
|
debug
|
|
read_sysfs
|
|
is_process_alive
|
|
read_lock_pid
|
|
acquire_exclusive_lock
|
|
ensure_pve_mod_directory_exists
|
|
check_executable
|
|
startup_message
|
|
setup_collector_signals
|
|
safe_write_json
|
|
safe_read_json
|
|
parse_csv_line
|
|
);
|
|
|
|
# ============================================================================
|
|
# Debug
|
|
# ============================================================================
|
|
|
|
# debug function showing line number and call chain
|
|
# Usage: debug(__LINE__, "message")
|
|
sub debug {
|
|
return unless $DEBUG_ENABLED;
|
|
|
|
my ($line, $message) = @_;
|
|
|
|
my @caller1 = caller(1); # who called debug()
|
|
my @caller2 = caller(2); # parent of caller
|
|
|
|
my $sub1 = $caller1[3] || 'main';
|
|
my $sub2 = $caller2[3];
|
|
|
|
$sub1 =~ s/.*:://;
|
|
|
|
my $output;
|
|
if (defined $sub2) {
|
|
$sub2 =~ s/.*:://;
|
|
$output = "[$sub2 -> $sub1:$line] $message\n";
|
|
} else {
|
|
$output = "[$sub1:$line] $message\n";
|
|
}
|
|
|
|
warn $output;
|
|
|
|
if ($config{debug}{log_enabled} && !defined $debug_log_fh) {
|
|
if (open(my $fh, '>>', $config{debug}{log_file})) {
|
|
$fh->autoflush(1);
|
|
$debug_log_fh = $fh;
|
|
} else {
|
|
warn "[debug] Failed to open log file $config{debug}{log_file}: $!\n";
|
|
}
|
|
}
|
|
print $debug_log_fh $output if defined $debug_log_fh;
|
|
}
|
|
|
|
# ============================================================================
|
|
# File / Process helpers
|
|
# ============================================================================
|
|
|
|
sub read_sysfs {
|
|
my ($path) = @_;
|
|
|
|
return "unknown" unless defined $path && -f $path;
|
|
|
|
if (open my $fh, '<', $path) {
|
|
my $value = <$fh>;
|
|
close $fh;
|
|
|
|
if (defined $value) {
|
|
chomp $value;
|
|
$value =~ s/^\s+|\s+$//g;
|
|
return $value ne '' ? $value : "unknown";
|
|
}
|
|
}
|
|
|
|
return "unknown";
|
|
}
|
|
|
|
sub is_process_alive {
|
|
my ($pid) = @_;
|
|
return -d "/proc/$pid";
|
|
}
|
|
|
|
sub read_lock_pid {
|
|
my ($lock_path) = @_;
|
|
|
|
return undef unless open(my $fh, '<', $lock_path);
|
|
|
|
my $pid = <$fh>;
|
|
close($fh);
|
|
chomp $pid if defined $pid;
|
|
|
|
return $pid;
|
|
}
|
|
|
|
sub acquire_exclusive_lock {
|
|
my ($lock_path, $purpose) = @_;
|
|
$purpose //= 'lock';
|
|
|
|
my $fh;
|
|
|
|
if (sysopen($fh, $lock_path, O_CREAT|O_EXCL|O_WRONLY, 0644)) {
|
|
debug(__LINE__, "Acquired $purpose on first try");
|
|
return $fh;
|
|
}
|
|
|
|
debug(__LINE__, ucfirst($purpose) . " exists, checking if stale");
|
|
|
|
my $lock_pid = read_lock_pid($lock_path);
|
|
|
|
if (!defined $lock_pid) {
|
|
debug(__LINE__, "Could not read $purpose file: $!");
|
|
return undef;
|
|
}
|
|
|
|
if ($lock_pid eq '' || $lock_pid !~ /^\d+$/) {
|
|
debug(__LINE__, "Invalid PID in $purpose: '" . ($lock_pid // 'undefined') . "', removing");
|
|
unlink($lock_path);
|
|
} elsif (is_process_alive($lock_pid)) {
|
|
debug(__LINE__, ucfirst($purpose) . " holder PID $lock_pid is still alive");
|
|
return undef;
|
|
} else {
|
|
debug(__LINE__, ucfirst($purpose) . " holder PID $lock_pid is dead, removing stale lock");
|
|
unlink($lock_path);
|
|
}
|
|
|
|
unless (sysopen($fh, $lock_path, O_CREAT|O_EXCL|O_WRONLY, 0644)) {
|
|
debug(__LINE__, "Failed to acquire $purpose on retry: $!");
|
|
return undef;
|
|
}
|
|
|
|
debug(__LINE__, "Acquired $purpose after removing stale lock");
|
|
return $fh;
|
|
}
|
|
|
|
sub ensure_pve_mod_directory_exists {
|
|
unless (-d $pve_mod_working_dir) {
|
|
debug(__LINE__, "Creating directory $pve_mod_working_dir");
|
|
unless (mkdir($pve_mod_working_dir, 0755)) {
|
|
debug(__LINE__, "Failed to create $pve_mod_working_dir: $!. PVE Mod cannot start.");
|
|
die "Failed to create $pve_mod_working_dir: $!";
|
|
}
|
|
debug(__LINE__, "Directory $pve_mod_working_dir created");
|
|
} else {
|
|
debug(__LINE__, "Directory $pve_mod_working_dir already exists");
|
|
}
|
|
}
|
|
|
|
# Returns 1 if executable exists, or debug mode is active with a debug file present.
|
|
# Returns 0 otherwise.
|
|
sub check_executable {
|
|
my ($exec_path, $type, $debug_mode_enabled, $debug_file) = @_;
|
|
|
|
if (defined $debug_mode_enabled && $debug_mode_enabled) {
|
|
if (defined $debug_file && -f $debug_file) {
|
|
debug(__LINE__, "Debug mode enabled for $type, using debug file: $debug_file");
|
|
return 1;
|
|
} elsif (defined $debug_file) {
|
|
debug(__LINE__, "Debug mode enabled for $type but debug file missing: $debug_file");
|
|
return 0;
|
|
} else {
|
|
debug(__LINE__, "Debug mode enabled for $type, skipping executable check for $exec_path");
|
|
return 1;
|
|
}
|
|
}
|
|
|
|
unless (-x $exec_path) {
|
|
debug(__LINE__, "$type executable not found or not executable: $exec_path");
|
|
return 0;
|
|
}
|
|
|
|
debug(__LINE__, "$type executable found: $exec_path");
|
|
return 1;
|
|
}
|
|
|
|
sub startup_message {
|
|
debug(__LINE__, "PVE Mod is being started. Version $VERSION");
|
|
}
|
|
|
|
# Setup common TERM/INT signal handlers for collector processes.
|
|
# $shutdown_ref is a scalar ref that will be set to 1 on signal.
|
|
sub setup_collector_signals {
|
|
my ($name, $shutdown_ref, $extra_cleanup) = @_;
|
|
|
|
$SIG{TERM} = sub {
|
|
debug(__LINE__, "Collector $name received SIGTERM");
|
|
$$shutdown_ref = 1;
|
|
$extra_cleanup->() if $extra_cleanup;
|
|
};
|
|
$SIG{INT} = sub {
|
|
debug(__LINE__, "Collector $name received SIGINT");
|
|
$$shutdown_ref = 1;
|
|
$extra_cleanup->() if $extra_cleanup;
|
|
};
|
|
}
|
|
|
|
# ============================================================================
|
|
# JSON helpers
|
|
# ============================================================================
|
|
|
|
sub safe_write_json {
|
|
my ($filepath, $data, $pretty) = @_;
|
|
$pretty //= 1;
|
|
|
|
eval {
|
|
open my $fh, '>', $filepath or die "Failed to open $filepath: $!";
|
|
my $json = $pretty ? JSON->new->pretty->encode($data) : encode_json($data);
|
|
print $fh $json;
|
|
close $fh;
|
|
debug(__LINE__, "Wrote JSON to $filepath");
|
|
};
|
|
if ($@) {
|
|
debug(__LINE__, "Error writing to $filepath: $@");
|
|
return 0;
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
sub safe_read_json {
|
|
my ($filepath, $as_string) = @_;
|
|
|
|
return unless -f $filepath;
|
|
|
|
my $result;
|
|
eval {
|
|
open my $fh, '<', $filepath or die "Failed to open $filepath: $!";
|
|
local $/;
|
|
my $json = <$fh>;
|
|
close $fh;
|
|
|
|
if ($as_string) {
|
|
$result = $json;
|
|
} else {
|
|
$result = decode_json($json);
|
|
}
|
|
debug(__LINE__, "Read JSON from $filepath");
|
|
};
|
|
if ($@) {
|
|
debug(__LINE__, "Error reading $filepath: $@");
|
|
return;
|
|
}
|
|
return $result;
|
|
}
|
|
|
|
# ============================================================================
|
|
# CSV helper
|
|
# ============================================================================
|
|
|
|
sub parse_csv_line {
|
|
my ($line, $expected_fields) = @_;
|
|
|
|
return unless $line;
|
|
$line =~ s/^\s+|\s+$//g;
|
|
|
|
my @values = map { s/^\s+|\s+$//gr } split(/,/, $line);
|
|
|
|
return unless !$expected_fields || @values >= $expected_fields;
|
|
return @values;
|
|
}
|
|
|
|
1;
|