diff --git a/app/system/resources/classes/bsd_system_information.php b/app/system/resources/classes/bsd_system_information.php index b2bd3f667..fa7f4e697 100644 --- a/app/system/resources/classes/bsd_system_information.php +++ b/app/system/resources/classes/bsd_system_information.php @@ -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])) { diff --git a/app/system/resources/classes/linux_system_information.php b/app/system/resources/classes/linux_system_information.php index c866febb7..a621274a2 100644 --- a/app/system/resources/classes/linux_system_information.php +++ b/app/system/resources/classes/linux_system_information.php @@ -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. * diff --git a/app/system/resources/classes/system_dashboard_service.php b/app/system/resources/classes/system_dashboard_service.php index 4a06ec01a..28e370aef 100644 --- a/app/system/resources/classes/system_dashboard_service.php +++ b/app/system/resources/classes/system_dashboard_service.php @@ -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 diff --git a/app/system/resources/classes/system_information.php b/app/system/resources/classes/system_information.php index 2b35db817..620124cc9 100644 --- a/app/system/resources/classes/system_information.php +++ b/app/system/resources/classes/system_information.php @@ -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; } } diff --git a/app/system/resources/dashboard/system_cpu_status.php b/app/system/resources/dashboard/system_cpu_status.php index ee6c35bea..a3afa4950 100644 --- a/app/system/resources/dashboard/system_cpu_status.php +++ b/app/system/resources/dashboard/system_cpu_status.php @@ -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 diff --git a/app/system/resources/dashboard/system_services.php b/app/system/resources/dashboard/system_services.php index c58f22ee3..74389f391 100644 --- a/app/system/resources/dashboard/system_services.php +++ b/app/system/resources/dashboard/system_services.php @@ -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));