Master-Branch-Fix freebsd dashboard items (#7967)

* Fix freebsd dashboard items

* Fix service total count
This commit is contained in:
frytimo
2026-05-12 19:15:41 +00:00
committed by GitHub
parent 1a286a3020
commit 7bf33e8668
6 changed files with 361 additions and 112 deletions
@@ -31,6 +31,99 @@
*/
class bsd_system_information extends system_information {
/**
* Selects the preferred service definition file for BSD.
*
* @param array $module_files Service definition files for a module.
*
* @return string|null Preferred file path.
*/
protected function select_preferred_service_file(array $module_files): ?string {
$selected_file = $module_files[0] ?? null;
if ($selected_file === null) {
return null;
}
foreach ($module_files as $candidate) {
if (basename($candidate) === 'freebsd.service') {
$selected_file = $candidate;
break;
}
}
return $selected_file;
}
/**
* Retrieves a BSD service identifier from an rc.d-style service definition.
*
* @param string $file Path to the service file.
*
* @return string Service identifier, or empty string if not found.
*/
protected function get_service_identifier(string $file): string {
if (!file_exists($file)) {
return '';
}
$content = file_get_contents($file);
if ($content === false) {
return '';
}
if (preg_match('/^#\s*PROVIDE:\s*(\S+)/mi', $content, $matches)) {
return trim($matches[1]);
}
if (preg_match('/^name\s*=\s*["\']([^"\']+)["\']/mi', $content, $matches)) {
return trim($matches[1]);
}
return '';
}
/**
* Checks if a process with the given name is currently running on BSD.
*
* @param string $name The name of the process to check for.
*
* @return array Process status including running flag, PID, and elapsed time.
*/
public function is_running(string $name): array {
$name = trim($name);
$safe_name = escapeshellarg($name);
$running = false;
$pid = null;
$etime = null;
$rc = 1;
exec("service $safe_name onestatus >/dev/null 2>&1", $out, $rc);
if ($rc === 0) {
$running = true;
$status_line = shell_exec("service $safe_name status 2>/dev/null");
if (preg_match('/pid\s+([0-9]+)/i', (string)$status_line, $m)) {
$pid = $m[1];
}
}
// Fallback for services with non-standard status output or process names.
if (!$running) {
$proc_name = ($name === 'postgresql') ? 'postgres' : $name;
$safe_proc = escapeshellarg($proc_name);
$pid_guess = trim((string)shell_exec("pgrep -f $safe_proc | head -n 1"));
if ($pid_guess !== '' && preg_match('/^\d+$/', $pid_guess)) {
$running = true;
$pid = $pid_guess;
}
}
if ($running && !empty($pid)) {
$etime = trim((string)shell_exec("ps -p " . escapeshellarg($pid) . " -o etime= | tr -d '\n'"));
}
return ['running' => $running, 'pid' => $pid, 'etime' => $etime];
}
/**
* Returns the network card information.
*
@@ -39,9 +132,17 @@ class bsd_system_information extends system_information {
* @return string|null The network card information or the default value.
*/
public function get_network_card(?string $default_value = null): ?string {
// Implementation for BSD systems
$result = shell_exec("ifconfig -a | head -n1 | awk '{print $1}'");
$network_card = trim($result, " :");
// Implementation for BSD systems - get first non-loopback interface
$result = shell_exec("ifconfig -l 2>/dev/null | awk '{print $1}' | head -n1");
$network_card = trim($result);
if (!$network_card) {
// Fallback: try em0 first (common on VMs), then other common BSD interfaces
foreach (['em0', 'igb0', 'ixl0', 're0', 'bge0'] as $iface) {
if (@file_exists("/sys/class/net/$iface") || shell_exec("ifconfig $iface 2>/dev/null") !== null) {
return $iface;
}
}
}
return $network_card ?: $default_value;
}
@@ -51,9 +152,15 @@ class bsd_system_information extends system_information {
* @return int The number of CPU cores.
*/
public function get_cpu_cores(): int {
$result = shell_exec("dmesg | grep -i --max-count 1 CPUs | sed 's/[^0-9]*//g'");
$cpu_cores = trim($result);
return $cpu_cores;
// Try sysctl first (more reliable on FreeBSD)
$result = @shell_exec("sysctl -n hw.ncpu 2>/dev/null");
if ($result && is_numeric(trim($result))) {
return intval(trim($result));
}
// Fallback to dmesg parsing
$result = @shell_exec("dmesg | grep -i --max-count 1 CPUs | sed 's/[^0-9]*//g' 2>/dev/null");
$cpu_cores = intval(trim($result));
return $cpu_cores > 0 ? $cpu_cores : 1;
}
//get the CPU details
@@ -64,14 +171,59 @@ class bsd_system_information extends system_information {
* @return float The current CPU usage percentage.
*/
public function get_cpu_percent(): float {
$result = shell_exec('ps -A -o pcpu');
$percent_cpu = 0;
foreach (explode("\n", $result) as $value) {
if (is_numeric($value)) {
$percent_cpu = $percent_cpu + $value;
static $last = null;
$read_cp_time = static function (): ?array {
$raw = @trim((string) shell_exec('sysctl -n kern.cp_time 2>/dev/null'));
if ($raw === '') {
return null;
}
$parts = array_map('intval', preg_split('/\s+/', $raw));
if (count($parts) < 5) {
return null;
}
return [
'user' => $parts[0],
'nice' => $parts[1],
'sys' => $parts[2],
'intr' => $parts[3],
'idle' => $parts[4],
'total' => array_sum(array_slice($parts, 0, 5)),
];
};
$current = $read_cp_time();
if ($current === null) {
return 0;
}
// Prime baseline on first call so we can calculate a meaningful delta.
if ($last === null) {
$last = $current;
usleep(200000);
$current = $read_cp_time();
if ($current === null) {
return 0;
}
}
return $percent_cpu;
$delta_total = $current['total'] - $last['total'];
$delta_idle = $current['idle'] - $last['idle'];
$last = $current;
if ($delta_total <= 0) {
return 0;
}
$usage = (1 - ($delta_idle / $delta_total)) * 100;
if ($usage < 0) {
$usage = 0;
}
if ($usage > 100) {
$usage = 100;
}
return round($usage, 2);
}
/**
@@ -80,7 +232,8 @@ class bsd_system_information extends system_information {
* @return string The system uptime in seconds.
*/
public function get_uptime() {
return shell_exec('uptime');
$result = @shell_exec('uptime 2>/dev/null');
return $result ?: 'unknown';
}
/**
@@ -138,18 +291,41 @@ class bsd_system_information extends system_information {
public function get_network_speed(string $interface = 'em0'): array {
static $last = [];
// Run netstat for the interface
// Validate interface exists by running netstat
$output = shell_exec("netstat -bI {$interface} 2>/dev/null");
if (!$output)
return ['rx_bps' => 0, 'tx_bps' => 0];
if (!$output) {
// Interface doesn't exist or error - return zeros and try to detect correct interface
if (!isset($last[$interface])) {
// Try to auto-detect valid interface
$fallback = $this->get_network_card();
if ($fallback && $fallback !== $interface) {
$output = shell_exec("netstat -bI {$fallback} 2>/dev/null");
if ($output) {
// Use fallback interface for future calls
$interface = $fallback;
} else {
return ['rx_bps' => 0, 'tx_bps' => 0];
}
} else {
return ['rx_bps' => 0, 'tx_bps' => 0];
}
} else {
return ['rx_bps' => 0, 'tx_bps' => 0];
}
}
$lines = explode("\n", trim($output));
if (count($lines) < 2)
return ['rx_bps' => 0, 'tx_bps' => 0];
$cols = preg_split('/\s+/', $lines[1]);
$rx_bytes = (int) $cols[6]; // Ibytes
$tx_bytes = (int) $cols[9]; // Obytes
$cols = preg_split('/\s+/', trim($lines[1]));
if (count($cols) < 11)
return ['rx_bps' => 0, 'tx_bps' => 0];
// FreeBSD netstat -bI layout:
// 0 Name 1 Mtu 2 Network 3 Address 4 Ipkts 5 Ierrs 6 Idrop 7 Ibytes 8 Opkts 9 Oerrs 10 Obytes 11 Coll
$rx_bytes = (int) $cols[7]; // Ibytes
$tx_bytes = (int) $cols[10]; // Obytes
$now = microtime(true);
if (!isset($last[$interface])) {
@@ -31,6 +31,72 @@
*/
class linux_system_information extends system_information {
/**
* Selects the preferred service definition file for Linux.
*
* @param array $module_files Service definition files for a module.
*
* @return string|null Preferred file path.
*/
protected function select_preferred_service_file(array $module_files): ?string {
$selected_file = $module_files[0] ?? null;
if ($selected_file === null) {
return null;
}
foreach ($module_files as $candidate) {
if (strpos(basename($candidate), 'debian') !== false) {
$selected_file = $candidate;
break;
}
}
return $selected_file;
}
/**
* Retrieves a Linux service identifier from a systemd service definition.
*
* @param string $file Path to the service file.
*
* @return string Service identifier, or empty string if not found.
*/
protected function get_service_identifier(string $file): string {
if (!file_exists($file)) {
return '';
}
$content = file_get_contents($file);
if ($content === false) {
return '';
}
if (preg_match('/^ExecStart\s*=\s*.*?\s+([^\s]+\.php)\s*$/mi', $content, $matches)) {
return basename($matches[1], '.php');
}
return '';
}
/**
* Checks if a process with the given name is currently running on Linux.
*
* @param string $name The name of the process to check for.
*
* @return array Process status including running flag, PID, and elapsed time.
*/
public function is_running(string $name): array {
$name = trim($name);
$safe_name = escapeshellarg($name);
$pid = trim((string)shell_exec("ps -aux | grep $safe_name | grep -v grep | awk '{print \$2}' | head -n 1"));
if ($pid !== '' && preg_match('/^\d+$/', $pid)) {
$etime = trim((string)shell_exec("ps -p $pid -o etime= | tr -d '\n'"));
return ['running' => true, 'pid' => $pid, 'etime' => $etime];
}
return ['running' => false, 'pid' => null, 'etime' => null];
}
/**
* Returns the number of CPU cores available on the system.
*
@@ -64,8 +64,14 @@ class system_dashboard_service extends base_websocket_system_service {
// get the network interval
$this->network_status_refresh_interval = intval($this->settings->get('system', 'network_status_refresh_interval', 3));
// get the network card to watch
$this->network_interface = $this->settings->get('system', 'network_interface', 'eth0');
// get the network card to watch - auto-detect if not configured
$configured_interface = $this->settings->get('system', 'network_interface', '');
if (!empty($configured_interface)) {
$this->network_interface = $configured_interface;
} else {
// Auto-detect network interface from system information
$this->network_interface = self::$system_information->get_network_card('em0');
}
}
/**
@@ -122,8 +128,8 @@ class system_dashboard_service extends base_websocket_system_service {
->topic(self::NETWORK_STATUS_TOPIC)
;
if ($message !== null && $message instanceof websocket_message) {
$this->debug("Responding to message request id: ".$message->id());
$response->id($message->id());
$this->debug("Responding to message request id: ".$message->request_id());
$response->request_id($message->request_id());
}
// Log for debugging
@@ -167,7 +173,7 @@ class system_dashboard_service extends base_websocket_system_service {
if ($message !== null && $message instanceof websocket_message) {
$payload = $message->payload();
if (!empty($payload['network_interface'])) {
$this->network_interface = ['network_interface'];
$this->network_interface = $payload['network_interface'];
}
}
}
@@ -202,7 +208,7 @@ class system_dashboard_service extends base_websocket_system_service {
// Include message ID if responding to a request
if ($message !== null && $message instanceof websocket_message) {
$response->id($message->id());
$response->request_id($message->request_id());
}
// Log for debugging
@@ -37,6 +37,9 @@ abstract class system_information {
abstract public function get_cpu_percent_per_core(): array;
abstract public function get_network_speed(string $interface = 'eth0'): array;
abstract public function get_network_card(?string $default_value = null): ?string;
abstract public function is_running(string $name): array;
abstract protected function get_service_identifier(string $file): string;
abstract protected function select_preferred_service_file(array $module_files): ?string;
/**
* Returns the system load average.
@@ -47,18 +50,71 @@ abstract class system_information {
return sys_getloadavg();
}
/**
* Builds installed service status entries from grouped service definition files.
*
* @param array $grouped_service_files Grouped service files keyed by module directory.
* @param array $service_labels Optional map of service key to display label.
*
* @return array Installed services keyed by service name.
*/
public function get_installed_services(array $grouped_service_files, array $service_labels = []): array {
$services = [];
foreach ($grouped_service_files as $module_files) {
if (!is_array($module_files) || empty($module_files)) {
continue;
}
$selected_file = $this->select_preferred_service_file($module_files);
if (empty($selected_file)) {
continue;
}
$service = $this->get_service_identifier($selected_file);
if (!empty($service)) {
$basename = basename($service, '.php');
$info = $this->is_running($basename);
$info['label'] = $service_labels[$basename] ?? ucwords(str_replace('_', ' ', $basename));
$services[$basename] = $info;
}
}
return $services;
}
/**
* Returns a system information object based on the underlying operating system.
*
* @return ?system_information The system information object for the current OS, or null if not supported.
*/
public static function new(): ?system_information {
if (stristr(PHP_OS, 'BSD')) {
return new bsd_system_information();
// Compatibility with PHP 7.1 and below
if (!defined('PHP_OS_FAMILY')) {
if (stripos(PHP_OS, 'linux') === 0) {
define('PHP_OS_FAMILY', 'Linux');
} elseif (stripos(PHP_OS, 'bsd') !== false) {
define('PHP_OS_FAMILY', 'BSD');
} elseif (stripos(PHP_OS, 'dar') === 0) {
define('PHP_OS_FAMILY', 'Darwin');
} elseif (stripos(PHP_OS, 'sunos') === 0) {
define('PHP_OS_FAMILY', 'Solaris');
} elseif (stripos(PHP_OS, 'win') === 0) {
define('PHP_OS_FAMILY', 'Windows');
} else {
define('PHP_OS_FAMILY', 'Unknown');
}
}
if (stristr(PHP_OS, 'Linux')) {
return new linux_system_information();
// Determine the class name based on the OS family
$class = strtolower(PHP_OS_FAMILY) . '_system_information';
if (class_exists($class)) {
// linux_system_information or bsd_system_information object
return new $class();
}
// Unsupported OS
return null;
}
}
@@ -32,28 +32,18 @@
$row_style["1"] = "row_style1";
//get the CPU details
if (stristr(PHP_OS, 'BSD') || stristr(PHP_OS, 'Linux')) {
$system_information = system_information::new();
if ($system_information !== null) {
$result = shell_exec('ps -A -o pcpu');
$percent_cpu = 0;
foreach (explode("\n", $result) as $value) {
if (is_numeric($value)) { $percent_cpu = $percent_cpu + $value; }
$percent_cpu = $system_information->get_cpu_percent();
$cpu_cores = $system_information->get_cpu_cores();
if ($cpu_cores < 1) {
$cpu_cores = 1;
}
if (stristr(PHP_OS, 'BSD')) {
$result = shell_exec("dmesg | grep -i --max-count 1 CPUs | sed 's/[^0-9]*//g'");
$cpu_cores = trim($result);
}
if (stristr(PHP_OS, 'Linux')) {
$result = @trim(shell_exec("grep -P '^processor' /proc/cpuinfo"));
$cpu_cores = count(explode("\n", $result));
}
if ($cpu_cores > 1) { $percent_cpu = $percent_cpu / $cpu_cores; }
$percent_cpu = round($percent_cpu, 2);
//uptime
$result = shell_exec('uptime');
$load_average = sys_getloadavg();
}
//show the content
@@ -36,51 +36,6 @@
exit;
}
//function to parse a FusionPBX service from a .service file
if (!function_exists('get_classname')) {
/**
* Retrieves the name of a PHP class from an ExecStart directive in a service file.
*
* @param string $file Path to the service file.
*
* @return string The name of the PHP class, or empty string if not found.
*/
function get_classname(string $file) {
if (!file_exists($file)) {
return '';
}
$parsed = parse_ini_file($file);
$exec_cmd = $parsed['ExecStart'] ?? '';
$parts = explode(' ', $exec_cmd ?? '');
$php_file = $parts[1] ?? '';
if (!empty($php_file)) {
return $php_file;
}
return '';
}
}
//function to check for running process: returns [running, pid, etime]
if (!function_exists('is_running')) {
/**
* Checks if a process with the given name is currently running.
*
* @param string $name The name of the process to check for.
*
* @return array An array containing information about the process's status,
* including whether it's running, its PID, and how long it's been running.
*/
function is_running(string $name) {
$name = escapeshellarg($name);
$pid = trim(shell_exec("ps -aux | grep $name | grep -v grep | awk '{print \$2}' | head -n 1") ?? '');
if ($pid && is_numeric($pid)) {
$etime = trim(shell_exec("ps -p $pid -o etime= | tr -d '\n'") ?? '');
return ['running' => true, 'pid' => $pid, 'etime' => $etime];
}
return ['running' => false, 'pid' => null, 'etime' => null];
}
}
//function to format etime into friendly display
if (!function_exists('format_etime')) {
/**
@@ -133,31 +88,29 @@
'event_guard' => 'Event Guard',
'fax_queue' => 'Fax Queue',
'maintenance_service' => 'Maintenance Service',
'message_events' => 'Message Events',
'message_queue' => 'Message Queue',
'xml_cdr' => 'XML CDR',
'freeswitch' => 'FreeSWITCH',
'nginx' => 'Nginx',
'postgresql' => 'PostgreSQL',
'event_guard' => 'Event Guard',
'sshd' => 'SSH Server'
'message_events' => 'Message Events',
'message_queue' => 'Message Queue',
'xml_cdr' => 'XML CDR',
'freeswitch' => 'FreeSWITCH',
'nginx' => 'Nginx',
'postgresql' => 'PostgreSQL',
'event_guard' => 'Event Guard',
'sshd' => 'SSH Server'
];
$files = glob(PROJECT_ROOT . '/*/*/resources/service/*.service');
$services = [];
$total_running = 0;
// load FusionPBX installed services
// Group files by module directory so debian/freebsd variants are counted once.
$grouped_service_files = [];
foreach ($files as $file) {
$service = get_classname($file);
//check if the service name was found
if (!empty($service)) {
$basename = basename($service, '.php');
$info = is_running($service);
$info['label'] = $service_labels[$basename] ?? ucwords(str_replace('_', ' ', $basename));
$services[$basename] = $info;
if ($info['running']) $total_running++;
}
$grouped_service_files[dirname($file)][] = $file;
}
// load FusionPBX installed services using OS-specific class handling
$system = system_information::new();
if ($system !== null) {
$services = $system->get_installed_services($grouped_service_files, $service_labels);
}
// Get extra system services from default settings
@@ -171,11 +124,10 @@
// Loop through extra services if array is not empty
if (!empty($extra_services)) {
foreach ($extra_services as $extra) {
if (!isset($services[$extra])) {
$info = is_running($extra);
if (!isset($services[$extra]) && $system !== null) {
$info = $system->is_running($extra);
$info['label'] = $service_labels[$extra] ?? ucwords($extra);
$services[$extra] = $info;
if ($info['running']) $total_running++;
}
}
}
@@ -183,6 +135,9 @@
//track total installed services for charts
$total_services = count($services);
$total_running = count(array_filter($services, function($service) {
return !empty($service['running']);
}));
//convert to a key
$widget_key = str_replace(' ', '_', strtolower($widget_name));