refactor code

This commit is contained in:
Meliox 2026-01-11 14:59:30 +01:00
parent d751c58f3a
commit 4e7396d719

View File

@ -4,7 +4,6 @@ use strict;
use warnings;
use JSON;
use POSIX qw(WNOHANG);
use Fcntl qw(:flock);
use Time::HiRes qw(time);
use Fcntl qw(:flock O_CREAT O_EXCL O_WRONLY);
use File::Path qw(remove_tree);
@ -13,6 +12,63 @@ use File::Path qw(remove_tree);
my $debug_ENABLED = 1;
my $VERSION = '1.0.0';
# ============================================================================
# Configuration
# ============================================================================
my %config = (
gpu => {
intel_enabled => 1,
amd_enabled => 0,
nvidia_enabled => 1,
},
debug => {
nvidia_mode => 1,
nvidia_devices_file => '/tmp/nvidia-smi-devices.csv',
nvidia_output_file => '/tmp/nvidia-smi-output.csv',
sensors_mode => 0,
sensors_output_file => '/tmp/sensors-output.json',
},
intervals => {
data_pull => 1, # seconds between data pulls
collector_timeout => 10, # stop collectors after N seconds of inactivity
},
ups => {
enabled => 1,
device_name => 'ups@192.168.3.2',
},
paths => {
working_dir => '/run/pveproxy/pve-mod',
},
);
# ============================================================================
# Derived paths and runtime state
# ============================================================================
# Derived paths from configuration
my $pve_mod_working_dir = $config{paths}{working_dir};
my $stats_dir = $pve_mod_working_dir;
my $state_file = "$pve_mod_working_dir/stats.json";
my $sensors_state_file = "$pve_mod_working_dir/sensors.json";
my $ups_state_file = "$pve_mod_working_dir/ups.json";
my $pve_mod_worker_lock = "$pve_mod_working_dir/pve_mod_worker.lock";
my $startup_lock = "$pve_mod_working_dir/startup.lock";
# Runtime state variables
my $process_type = 'main'; # 'main', 'worker', or 'collector'
my $last_snapshot = {};
my $last_mtime = 0;
my $last_get_graphic_stats_time = 0;
my $pve_mod_worker_pid;
my $pve_mod_worker_running = 0;
# Collector registry - only populated in worker process
my %collectors = (); # key: device/card name, value: PID
# ============================================================================
# Shared Utility Functions
# ============================================================================
# debug function showing line number and call chain
# Usage: _debug(__LINE__, "message")
sub _debug {
@ -38,44 +94,266 @@ sub _debug {
}
}
my $pve_mod_working_dir = '/run/pveproxy/pve-mod';
my $stats_dir = $pve_mod_working_dir;
my $state_file = "$pve_mod_working_dir/stats.json";
my $sensors_state_file = "$pve_mod_working_dir/sensors.json";
my $ups_state_file = "$pve_mod_working_dir/ups.json";
sub read_sysfs {
my ($path) = @_;
my $pve_mod_worker_lock = "$pve_mod_working_dir/pve_mod_worker.lock"; # PVE Mod worker lock
my $startup_lock = "$pve_mod_working_dir/startup.lock"; # Exclusive startup lock
my $last_snapshot = {};
my $last_mtime = 0;
return "unknown" unless defined $path && -f $path;
# Collector registry - only populated in worker process
my %collectors = (); # key: device/card name, value: PID
my $last_get_graphic_stats_time = 0; # Track when get_graphic_stats was last called
my $COLLECTOR_TIMEOUT = 10; # Stop collectors x seconds after last get_graphic_stats call
my $data_pull_interval = 1; # Interval in seconds between data pulls
if (open my $fh, '<', $path) {
my $value = <$fh>;
close $fh;
my $intel_gpu_enabled = 1; # Set to 0 to disable Intel GPU support
my $amd_gpu_enabled = 0; # Set to 1 to enable AMD GPU support (not yet implemented)
my $nvidia_gpu_enabled = 1; # Set to 1 to enable NVIDIA GPU support (not yet implemented)
my $nvidia_debug_mode = 1; # Set to 1 to enable NVIDIA debug mode (load from files instead of nvidia-smi)
my $nvidia_debug_devices = '/tmp/nvidia-smi-devices.csv';
my $nvidia_debug_output = '/tmp/nvidia-smi-output.csv';
my $sensors_debug_mode = 0; # Set to 1 to enable sensors debug mode (load from file instead of sensors command)
my $sensors_debug_output = '/tmp/sensors-output.json';
my $ups_enabled = 1; # Set to 1 to enable UPS support
my $pve_mod_worker_pid;
my $pve_mod_worker_running = 0;
if (defined $value) {
chomp $value;
# Remove leading/trailing whitespace
$value =~ s/^\s+|\s+$//g;
return $value ne '' ? $value : "unknown";
}
}
# UPS Configuration
my $ups_device = {
ups_name => 'ups@192.168.3.2', # Format: upsname[@hostname[:port]]
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;
# Try to create lock file exclusively
if (sysopen($fh, $lock_path, O_CREAT|O_EXCL|O_WRONLY, 0644)) {
_debug(__LINE__, "Acquired $purpose on first try");
return $fh;
}
# Lock file creation failed - check if it's stale or held by another process
_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);
}
# Try to acquire lock again after cleanup
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 _is_lock_stale {
my ($lock_path) = @_;
return 0 unless open(my $fh, '<', $lock_path);
my $lock_pid = <$fh>;
chomp $lock_pid if defined $lock_pid;
close($fh);
# Invalid or missing PID
return 1 unless defined $lock_pid && $lock_pid =~ /^\d+$/;
# Valid PID but process is dead
return !_is_process_alive($lock_pid);
}
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");
}
}
# Generic function to check if required executable exists
# Returns 1 if executable exists or debug mode is enabled for that type
# Returns 0 if executable doesn't exist and debug mode is not enabled
sub _check_executable {
my ($exec_path, $type, $debug_mode_enabled, $debug_file) = @_;
# If debug mode is enabled for this type, check if debug file exists instead
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;
}
}
# Normal mode: check if executable exists
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 _pve_mod_hello {
_debug(__LINE__, "PVE Mod is being started. Version $VERSION");
}
# Setup common signal handlers for collector processes
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;
};
}
# ============================================================================
# Code starts here
# ============================================================================
my $process_type = 'main'; # 'main', 'worker', or 'collector'
# Safe JSON file write with error handling
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;
}
# Safe JSON file read with error handling
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;
}
# Parse CSV line with trimming
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;
}
# Enhance sensors data with cached lookups (unified for drives and CPUs)
sub _enhance_sensors_with_cache {
my ($sensors_output, $cache_ref, $pattern, $lookup_sub, $field_names) = @_;
$cache_ref //= {};
my $sensors_data = _safe_read_json(\$sensors_output);
return $sensors_output unless $sensors_data;
# For string input, parse it
unless (ref $sensors_data eq 'HASH') {
eval { $sensors_data = decode_json($sensors_output); };
return $sensors_output if $@;
}
my @entries = grep { /$pattern/ } keys %{$sensors_data};
_debug(__LINE__, "Found " . scalar(@entries) . " entries matching pattern");
foreach my $entry (@entries) {
my $metadata;
# Check cache first
if (exists $cache_ref->{$entry}) {
$metadata = $cache_ref->{$entry};
_debug(__LINE__, "Using cached info for $entry");
} else {
# Lookup information
$metadata = $lookup_sub->($entry);
$cache_ref->{$entry} = $metadata if $metadata;
}
# Add metadata to sensors data
if ($metadata && exists $sensors_data->{$entry}) {
foreach my $key (keys %$metadata) {
$sensors_data->{$entry}->{$key} = $metadata->{$key};
}
_debug(__LINE__, "Enhanced $entry with metadata");
}
}
return JSON->new->pretty->canonical->encode($sensors_data);
}
# ============================================================================
# Intel GPU Support
@ -201,20 +479,13 @@ sub _collector_for_intel_device {
# Set up signal handlers for graceful shutdown
my $shutdown = 0;
$SIG{TERM} = sub {
_debug(__LINE__, "Collector for $device->{card} received SIGTERM");
$shutdown = 1;
_setup_collector_signals($device->{card}, \$shutdown, sub {
kill 'TERM', $intel_gpu_top_pid if defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0;
};
$SIG{INT} = sub {
_debug(__LINE__, "Collector for $device->{card} received SIGINT");
$shutdown = 1;
kill 'TERM', $intel_gpu_top_pid if defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0;
};
});
# Run intel_gpu_top once and keep reading from it
_debug(__LINE__, "About to open pipe to intel_gpu_top");
my $intel_pull_interval = $data_pull_interval * 1000; # in milliseconds
my $intel_pull_interval = $config{intervals}{data_pull} * 1000; # in milliseconds
$intel_gpu_top_pid = open(my $fh, '-|', "intel_gpu_top -d $drm_dev -s $intel_pull_interval -l 2>&1");
unless (defined $intel_gpu_top_pid && $intel_gpu_top_pid > 0) {
@ -252,15 +523,7 @@ sub _collector_for_intel_device {
};
# Write to device-specific file
eval {
open my $ofh, '>', $device_state_file or die "Failed to open $device_state_file: $!";
print $ofh JSON->new->pretty->encode($device_data);
close $ofh;
_debug(__LINE__, "Wrote stats to $device_state_file (line #$line_count)");
};
if ($@) {
_debug(__LINE__, "Error writing stats: $@");
}
_safe_write_json($device_state_file, $device_data);
}
}
}
@ -320,13 +583,13 @@ sub get_nvidia_gpu_devices {
# 1, NVIDIA RTX A4000
# Check if nvidia-smi is available (or debug mode with debug file)
unless (_check_executable('/usr/bin/nvidia-smi', 'NVIDIA', $nvidia_debug_mode, $nvidia_debug_devices)) {
unless (_check_executable('/usr/bin/nvidia-smi', 'NVIDIA', $config{debug}{nvidia_mode}, $config{debug}{nvidia_devices_file})) {
return @devices;
}
if ($nvidia_debug_mode && -f $nvidia_debug_devices) {
_debug(__LINE__, "Debug mode: reading NVIDIA GPU devices from $nvidia_debug_devices");
if (open my $fh, '<', $nvidia_debug_devices) {
if ($config{debug}{nvidia_mode} && -f $config{debug}{nvidia_devices_file}) {
_debug(__LINE__, "Debug mode: reading NVIDIA GPU devices from $config{debug}{nvidia_devices_file}");
if (open my $fh, '<', $config{debug}{nvidia_devices_file}) {
my $line_num = 0;
while (<$fh>) {
chomp;
@ -335,20 +598,19 @@ sub get_nvidia_gpu_devices {
# Skip header line and empty lines
next if $line_num == 1 || /^\s*$/;
# Parse CSV: "0, NVIDIA GeForce RTX 3080"
if (/^\s*(\d+)\s*,\s*(.+?)\s*$/) {
my $index = $1;
my $name = $2;
# Parse CSV using shared helper
my @values = _parse_csv_line($_, 2);
if (@values) {
push @devices, {
name => $name,
index => $index,
index => $values[0],
name => $values[1],
};
_debug(__LINE__, "Found NVIDIA GPU device (debug): $name -> (index: $index)");
_debug(__LINE__, "Found NVIDIA GPU device (debug): $values[1] -> (index: $values[0])");
}
}
close $fh;
} else {
_debug(__LINE__, "Failed to open debug file $nvidia_debug_devices: $!");
_debug(__LINE__, "Failed to open debug file $config{debug}{nvidia_devices_file}: $!");
}
} else {
# Use nvidia-smi to get device list
@ -361,15 +623,14 @@ sub get_nvidia_gpu_devices {
# Skip header line and empty lines
next if $line_num == 1 || /^\s*$/;
# Parse CSV: "0, NVIDIA GeForce RTX 3080"
if (/^\s*(\d+)\s*,\s*(.+?)\s*$/) {
my $index = $1;
my $name = $2;
# Parse CSV using shared helper
my @values = _parse_csv_line($_, 2);
if (@values) {
push @devices, {
name => $name,
index => $index,
index => $values[0],
name => $values[1],
};
_debug(__LINE__, "Found NVIDIA GPU device: $name -> (index: $index)");
_debug(__LINE__, "Found NVIDIA GPU device: $values[1] -> (index: $values[0])");
}
}
close $fh;
@ -386,17 +647,9 @@ sub parse_nvidia_gpu_line {
# index, name, temperature.gpu, utilization.gpu, utilization.memory, memory.used, memory.total, power.draw, power.limit, fan.speed
#0, NVIDIA GeForce RTX 3080, 62, 79, 44, 8260, 10240, 268.12, 320.00, 67
# Remove leading/trailing whitespace
$line =~ s/^\s+|\s+$//g;
# Skip empty lines
return unless $line;
# Split by comma and trim whitespace from each field
my @values = map { s/^\s+|\s+$//gr } split(/,/, $line);
# Expected: index(0), name(1), temp(2), util_gpu(3), util_mem(4), mem_used(5), mem_total(6), power_draw(7), power_limit(8), fan_speed(9)
return unless @values >= 10;
# Parse CSV using shared helper
my @values = _parse_csv_line($line, 10);
return unless @values;
my $stats = {
index => $values[0] + 0,
@ -433,10 +686,10 @@ sub _get_and_write_nvidia_stats {
my ($devices) = @_;
my @all_stats;
if ($nvidia_debug_mode && -f $nvidia_debug_output) {
if ($config{debug}{nvidia_mode} && -f $config{debug}{nvidia_output_file}) {
# Debug mode: read all GPUs from single file
_debug(__LINE__, "Debug mode: reading NVIDIA GPU stats from $nvidia_debug_output");
if (open my $fh, '<', $nvidia_debug_output) {
_debug(__LINE__, "Debug mode: reading NVIDIA GPU stats from $config{debug}{nvidia_output_file}");
if (open my $fh, '<', $config{debug}{nvidia_output_file}) {
my $line_num = 0;
while (<$fh>) {
chomp;
@ -451,7 +704,7 @@ sub _get_and_write_nvidia_stats {
}
close $fh;
} else {
_debug(__LINE__, "Failed to open debug file $nvidia_debug_output: $!");
_debug(__LINE__, "Failed to open debug file $config{debug}{nvidia_output_file}: $!");
}
} else {
# Production mode: check if nvidia-smi is available before querying
@ -514,15 +767,7 @@ sub _get_and_write_nvidia_stats {
};
# Write to device-specific file
eval {
open my $ofh, '>', $device_state_file or die "Failed to open $device_state_file: $!";
print $ofh JSON->new->pretty->encode($device_data);
close $ofh;
_debug(__LINE__, "Wrote NVIDIA GPU $device_index stats to $device_state_file");
};
if ($@) {
_debug(__LINE__, "Error writing NVIDIA stats for GPU $device_index: $@");
}
_safe_write_json($device_state_file, $device_data);
}
unless (@all_stats) {
@ -542,14 +787,7 @@ sub _collector_for_nvidia_devices {
# Set up signal handlers for graceful shutdown
my $shutdown = 0;
$SIG{TERM} = sub {
_debug(__LINE__, "NVIDIA collector received SIGTERM");
$shutdown = 1;
};
$SIG{INT} = sub {
_debug(__LINE__, "NVIDIA collector received SIGINT");
$shutdown = 1;
};
_setup_collector_signals('nvidia-all', \$shutdown);
# Expected CSV format (with header):
# index, name, temperature.gpu, utilization.gpu, utilization.memory, memory.used, memory.total, power.draw, power.limit, fan.speed
@ -560,42 +798,13 @@ sub _collector_for_nvidia_devices {
# Collect and write NVIDIA GPU stats
_get_and_write_nvidia_stats($devices);
sleep $data_pull_interval unless $shutdown;
sleep $config{intervals}{data_pull} unless $shutdown;
}
_debug(__LINE__, "NVIDIA collector shutting down");
exit 0;
}
# ============================================================================
# Unified Child Process Management
# ============================================================================
sub _start_child_collector {
my ($collector_name, $collector_sub, $device) = @_;
_debug(__LINE__, "Starting child collector: $collector_name");
my $pid = fork();
unless (defined $pid) {
_debug(__LINE__, "fork failed for $collector_name: $!");
return undef;
}
if ($pid == 0) {
# Child process
$process_type = 'collector';
_debug(__LINE__, "In child process for $collector_name");
$0 = "collector-$collector_name";
$collector_sub->($device);
exit(0);
}
# Parent process (worker only)
_debug(__LINE__, "Forked child PID $pid for $collector_name");
return $pid;
}
# ============================================================================
# Temperature Sensors
# ============================================================================
@ -608,7 +817,7 @@ sub _collector_for_temperature_sensors {
_debug(__LINE__, "Temperature sensor collector started");
# Check if lm-sensors is available (or debug mode with debug file)
unless (_check_executable('/usr/bin/sensors', 'lm-sensors', $sensors_debug_mode, $sensors_debug_output)) {
unless (_check_executable('/usr/bin/sensors', 'lm-sensors', $config{debug}{sensors_mode}, $config{debug}{sensors_output_file})) {
_debug(__LINE__, "sensors not available and not in debug mode, exiting");
exit(1);
}
@ -618,19 +827,12 @@ sub _collector_for_temperature_sensors {
# Set up signal handlers for graceful shutdown
my $shutdown = 0;
$SIG{TERM} = sub {
_debug(__LINE__, "Temperature sensor collector received SIGTERM");
$shutdown = 1;
};
$SIG{INT} = sub {
_debug(__LINE__, "Temperature sensor collector received SIGINT");
$shutdown = 1;
};
_setup_collector_signals('temperature-sensors', \$shutdown);
while (!$shutdown) {
my $sensorsData = _get_temperature_sensors(\%cache_ref);
# Write to sensors state file
# Write to sensors state file (as string, not parsed JSON)
eval {
open my $ofh, '>', $sensors_state_file or die "Failed to open $sensors_state_file: $!";
print $ofh $sensorsData;
@ -641,7 +843,7 @@ sub _collector_for_temperature_sensors {
_debug(__LINE__, "Error writing temperature sensor data: $@");
}
sleep $data_pull_interval unless $shutdown;
sleep $config{intervals}{data_pull} unless $shutdown;
}
_debug(__LINE__, "Temperature sensor collector shutting down");
@ -654,16 +856,16 @@ sub _get_temperature_sensors {
my $sensorsOutput;
# Collect sensor data from lm-sensors
if ($sensors_debug_mode && -f $sensors_debug_output) {
if ($config{debug}{sensors_mode} && -f $config{debug}{sensors_output_file}) {
# Debug mode: read from file
_debug(__LINE__, "Debug mode: reading sensors data from $sensors_debug_output");
if (open my $fh, '<', $sensors_debug_output) {
_debug(__LINE__, "Debug mode: reading sensors data from $config{debug}{sensors_output_file}");
if (open my $fh, '<', $config{debug}{sensors_output_file}) {
local $/;
$sensorsOutput = <$fh>;
close $fh;
_debug(__LINE__, "Read sensors data from debug file, length: " . length($sensorsOutput) . " bytes");
} else {
_debug(__LINE__, "Failed to open debug file $sensors_debug_output: $!");
_debug(__LINE__, "Failed to open debug file $config{debug}{sensors_output_file}: $!");
$sensorsOutput = '{}';
}
} else {
@ -1077,18 +1279,12 @@ sub _collector_for_ups {
# Set up signal handlers for graceful shutdown
my $shutdown = 0;
$SIG{TERM} = sub {
_debug(__LINE__, "UPS collector received SIGTERM");
$shutdown = 1;
};
$SIG{INT} = sub {
_debug(__LINE__, "UPS collector received SIGINT");
$shutdown = 1;
};
_setup_collector_signals("ups-$device->{ups_name}", \$shutdown);
while (!$shutdown) {
my $upsData = _get_ups_status($device->{ups_name});
# Write to ups state file
# Write to ups state file (as string, not parsed JSON)
eval {
open my $ofh, '>', $ups_state_file or die "Failed to open $ups_state_file: $!";
print $ofh $upsData;
@ -1099,7 +1295,7 @@ sub _collector_for_ups {
_debug(__LINE__, "Error writing ups data: $@");
}
sleep $data_pull_interval unless $shutdown;
sleep $config{intervals}{data_pull} unless $shutdown;
}
_debug(__LINE__, "UPS collector shutting down");
exit 0;
@ -1183,153 +1379,6 @@ sub _parse_upsc_output {
return $ups_data;
}
# ============================================================================
# Supporting functions
# ============================================================================
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;
# Remove leading/trailing whitespace
$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;
# Try to create lock file exclusively
if (sysopen($fh, $lock_path, O_CREAT|O_EXCL|O_WRONLY, 0644)) {
_debug(__LINE__, "Acquired $purpose on first try");
return $fh;
}
# Lock file creation failed - check if it's stale or held by another process
_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);
}
# Try to acquire lock again after cleanup
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 _is_lock_stale {
my ($lock_path) = @_;
return 0 unless open(my $fh, '<', $lock_path);
my $lock_pid = <$fh>;
chomp $lock_pid if defined $lock_pid;
close($fh);
# Invalid or missing PID
return 1 unless defined $lock_pid && $lock_pid =~ /^\d+$/;
# Valid PID but process is dead
return !_is_process_alive($lock_pid);
}
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");
}
}
# Generic function to check if required executable exists
# Returns 1 if executable exists or debug mode is enabled for that type
# Returns 0 if executable doesn't exist and debug mode is not enabled
sub _check_executable {
my ($exec_path, $type, $debug_mode_enabled, $debug_file) = @_;
# If debug mode is enabled for this type, check if debug file exists instead
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;
}
}
# Normal mode: check if executable exists
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 _pve_mod_hello {
_debug(__LINE__, "PVE Mod is being started. Version $VERSION");
}
# ============================================================================
# API calls
# ============================================================================
@ -1550,7 +1599,7 @@ sub _start_collector {
sub _start_graphics_collectors {
if ($intel_gpu_enabled == 0 && $amd_gpu_enabled == 0 && $nvidia_gpu_enabled == 0) {
if (!$config{gpu}{intel_enabled} && !$config{gpu}{amd_enabled} && !$config{gpu}{nvidia_enabled}) {
_debug(__LINE__, "No GPU types enabled, skipping collector startup");
return;
}
@ -1564,7 +1613,7 @@ sub _start_graphics_collectors {
my @all_collector_subs;
# Intel
if ($intel_gpu_enabled) {
if ($config{gpu}{intel_enabled}) {
_debug(__LINE__, "Intel GPU support enabled");
_debug(__LINE__, "Checking for intel_gpu_top");
@ -1584,7 +1633,7 @@ sub _start_graphics_collectors {
}
# AMD (future)
if ($amd_gpu_enabled) {
if ($config{gpu}{amd_enabled}) {
_debug(__LINE__, "AMD GPU support enabled");
return unless _check_executable('/usr/bin/rocm-smi', 'AMD');
@ -1604,7 +1653,7 @@ sub _start_graphics_collectors {
my $started_count = 0;
# NVIDIA - single collector for all devices
if ($nvidia_gpu_enabled) {
if ($config{gpu}{nvidia_enabled}) {
_debug(__LINE__, "NVIDIA GPU support enabled");
my @nvidia_devices = get_nvidia_gpu_devices();
@ -1633,7 +1682,7 @@ sub _start_sensors_collector {
_debug(__LINE__, "Starting temperature sensor collector");
# Check if sensors is available (or debug mode with debug file)
unless (_check_executable('/usr/bin/sensors', 'lm-sensors', $sensors_debug_mode, $sensors_debug_output)) {
unless (_check_executable('/usr/bin/sensors', 'lm-sensors', $config{debug}{sensors_mode}, $config{debug}{sensors_output_file})) {
_debug(__LINE__, "sensors not available and not in debug mode, skipping");
return;
}
@ -1644,7 +1693,7 @@ sub _start_sensors_collector {
sub _start_ups_collector {
if ($ups_enabled == 0) {
if (!$config{ups}{enabled}) {
_debug(__LINE__, "UPS support not enabled, skipping collector startup");
return;
}
@ -1658,19 +1707,44 @@ sub _start_ups_collector {
}
# Check if UPS is configured
unless (defined $ups_device && $ups_device->{ups_name}) {
unless ($config{ups}{device_name}) {
_debug(__LINE__, "No UPS configured, skipping collector startup");
return;
}
# Use unified collector startup
_start_collector('ups', 'ups', \&_collector_for_ups, $ups_device);
_start_collector('ups', 'ups', \&_collector_for_ups, { ups_name => $config{ups}{device_name} });
}
# ============================================================================
# PVE Mod Worker
# ============================================================================
sub _start_child_collector {
my ($collector_name, $collector_sub, $device) = @_;
_debug(__LINE__, "Starting child collector: $collector_name");
my $pid = fork();
unless (defined $pid) {
_debug(__LINE__, "fork failed for $collector_name: $!");
return undef;
}
if ($pid == 0) {
# Child process
$process_type = 'collector';
_debug(__LINE__, "In child process for $collector_name");
$0 = "collector-$collector_name";
$collector_sub->($device);
exit(0);
}
# Parent process (worker only)
_debug(__LINE__, "Forked child PID $pid for $collector_name");
return $pid;
}
sub _pve_mod_starter {
# Check if pve_mod_worker is already running - if so, entire system is already up
_debug(__LINE__, "Checking if pve_mod_worker is already running");
@ -1817,13 +1891,13 @@ sub _pve_mod_keep_alive {
$SIG{TERM} = sub {
_debug(__LINE__, "pve_mod_worker received SIGTERM, shutting down");
_stop_collectors();
_stop_child_collectors();
unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock;
exit(0);
};
$SIG{INT} = sub {
_debug(__LINE__, "pve_mod_worker received SIGINT, shutting down");
_stop_collectors();
_stop_child_collectors();
unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock;
exit(0);
};
@ -1835,18 +1909,18 @@ sub _pve_mod_keep_alive {
_start_ups_collector();
_debug(__LINE__, "All collectors started by worker");
_debug(__LINE__, "Entering pve_mod_worker loop, timeout=${COLLECTOR_TIMEOUT}s");
_debug(__LINE__, "Entering pve_mod_worker loop, timeout=$config{intervals}{collector_timeout}s");
while (1) {
_debug(__LINE__, "pve_mod_worker loop start: checking activity");
my $idle_time = time() - $last_activity;
_debug(__LINE__, "pve_mod_worker loop: idle_time=${idle_time}s, timeout=${COLLECTOR_TIMEOUT}s");
_debug(__LINE__, "pve_mod_worker loop: idle_time=${idle_time}s, timeout=$config{intervals}{collector_timeout}s");
if ($idle_time > $COLLECTOR_TIMEOUT) {
if ($idle_time > $config{intervals}{collector_timeout}) {
_debug(__LINE__, "Timeout reached, stopping collectors");
_stop_collectors();
_stop_child_collectors();
_debug(__LINE__, "Collectors stopped, exiting pve_mod_worker");
unlink($pve_mod_worker_lock) if -f $pve_mod_worker_lock;
exit(0);
@ -1862,11 +1936,7 @@ sub _is_pve_mod_worker_running {
return -f $pve_mod_worker_lock;
}
# ============================================================================
# Other
# ============================================================================
sub _stop_collectors {
sub _stop_child_collectors {
_debug(__LINE__, "Stopping all collectors");
# Get PIDs from worker's collector registry
@ -1926,7 +1996,7 @@ sub _stop_collectors {
END {
if ($process_type eq 'worker') {
_debug(__LINE__, "PVE Mod Worker END block: cleaning up");
_stop_collectors();
_stop_child_collectors();
} elsif ($process_type eq 'collector') {
_debug(__LINE__, "Collector ($0) END block: no cleanup needed");
# Collectors just exit, no cleanup needed