diff --git a/.travis.yml b/.travis.yml index b3681e1..912d3d6 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,14 +1,18 @@ language: php +dist: xenial + arch: - amd64 -- ppc64le - arm64 php: - 7.4 - 8.0 -- nightly +- 8.0.23 + +before_install: + - git clone https://github.com/P-H-C/phc-winner-argon2.git libargon2 && cd libargon2 && make test && sudo make install PREFIX=/usr && cd .. notifications: email: @@ -20,5 +24,4 @@ script: - vendor/bin/phpunit --configuration phpunit.xml --testsuite General - vendor/bin/psalm --show-info=true - if [ "$TRAVIS_CPU_ARCH" = "amd64" ]; then vendor/bin/phpunit --configuration phpunit.xml --testsuite X86; fi -- if [ "$TRAVIS_CPU_ARCH" = "ppc64le" ]; then vendor/bin/phpunit --configuration phpunit.xml --testsuite PPC; fi - if [ "$TRAVIS_CPU_ARCH" = "arm64" ]; then vendor/bin/phpunit --configuration phpunit.xml --testsuite ARM; fi diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..668b676 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,39 @@ +FROM composer:2.0 as composer + +ARG TESTING=false +ENV TESTING=$TESTING + +WORKDIR /usr/local/src/ + +COPY composer.lock /usr/local/src/ +COPY composer.json /usr/local/src/ + +RUN composer update --ignore-platform-reqs --optimize-autoloader \ + --no-plugins --no-scripts --prefer-dist + +FROM php:8.0-cli-alpine as compile + +RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone + +RUN \ + apk update \ + && apk add --no-cache make automake autoconf gcc g++ git brotli-dev \ + && docker-php-ext-install opcache \ + && rm -rf /var/cache/apk/* + +FROM compile as final + +LABEL maintainer="team@appwrite.io" + +WORKDIR /usr/src/code + +RUN mv "$PHP_INI_DIR/php.ini-production" "$PHP_INI_DIR/php.ini" + +RUN echo "opcache.enable_cli=1" >> $PHP_INI_DIR/php.ini + +COPY --from=composer /usr/local/src/vendor /usr/src/code/vendor + +# Add Source Code +COPY . /usr/src/code + +CMD [ "tail", "-f", "/dev/null" ] diff --git a/README.md b/README.md index a8a6a45..10a27af 100644 --- a/README.md +++ b/README.md @@ -36,6 +36,13 @@ echo System::isX86(); // bool Utopia Framework requires PHP 7.4 or later. We recommend using the latest PHP version whenever possible. +## Supported Methods +| | getCPUCores | getCPUUtilisation | getMemoryTotal | getMemoryFree | getDiskTotal | getDiskFree | getIOUsage | getNetworkUsage | +|---------|-------------|-------------------|----------------|---------------|--------------|-------------|------------|-----------------| +| Windows | ✅ | | | | ✅ | ✅ | | | +| MacOS | ✅ | | ✅ | ✅ | ✅ | ✅ | | | +| Linux | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ | + ## Authors **Eldad Fux** diff --git a/composer.json b/composer.json index 5c27a0a..de97f14 100644 --- a/composer.json +++ b/composer.json @@ -23,6 +23,7 @@ }, "require-dev": { "phpunit/phpunit": "^9.3", - "vimeo/psalm": "4.0.1" + "vimeo/psalm": "4.0.1", + "squizlabs/php_codesniffer": "^3.6" } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..2131adb --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,12 @@ +version: '3.1' + +services: + main-test: + build: + context: . + mem_limit: 512m + mem_reservation: 128M + cpus: 0.5 + command: tail -f /dev/null + volumes: + - ./:/usr/src/code diff --git a/src/System/System.php b/src/System/System.php index 6131249..5552255 100644 --- a/src/System/System.php +++ b/src/System/System.php @@ -14,8 +14,50 @@ class System private const RegExARM = '/(aarch*|arm*)/'; private const RegExPPC = '/(ppc*)/'; + /** + * A list of Linux Disks that are not considered valid + * These are usually virtual drives or other non-physical devices such as loopback or ram. + * + * This list is ran through a contains, meaning for example if 'loop' was in the list, + * A 'loop0' interface would be considered invalid and not computed. + * + * Documentation: + * Loop - https://man7.org/linux/man-pages/man4/loop.4.html + * Ram - https://man7.org/linux/man-pages/man4/ram.4.html + */ + private const INVALIDDISKS = [ + 'loop', + 'ram', + ]; + + /** + * A list of Linux Network Interfaces that are not considered valid + * These are usually virtual interfaces created by tools such as Docker or VirtualBox + * + * This list is ran through a contains, meaning for example if 'vboxnet' was in the list, + * A 'vboxnet0' interface would be considered invalid and not computed. + * + * Documentation: + * veth - https://man7.org/linux/man-pages/man4/veth.4.html + * docker - https://docs.docker.com/network/ + * lo - Localhost Loopback device, https://man7.org/linux/man-pages/man4/loop.4.html + * tun - Linux Layer 3 Interface, https://www.kernel.org/doc/html/v5.8/networking/tuntap.html + * vboxnet - Virtual Machine Networking Interface, https://www.virtualbox.org/manual/ch06.html + * bonding_masters - https://www.kernel.org/doc/Documentation/networking/bonding.txt + */ + private const INVALIDNETINTERFACES = [ + 'veth', + 'docker', + 'lo', + 'tun', + 'vboxnet', + '.', + 'bonding_masters' + ]; + /** * Returns the system's OS. + * * @return string */ static public function getOS(): string @@ -44,19 +86,19 @@ static public function getArchEnum(): string { $arch = self::getArch(); switch (1) { - case preg_match(self::RegExX86, $arch): - return System::X86; - break; - case preg_match(self::RegExPPC, $arch): - return System::PPC; - break; - case preg_match(self::RegExARM, $arch): - return System::ARM; - break; + case preg_match(self::RegExX86, $arch): + return System::X86; + break; + case preg_match(self::RegExPPC, $arch): + return System::PPC; + break; + case preg_match(self::RegExARM, $arch): + return System::ARM; + break; - default: - throw new Exception("'{$arch}' enum not found."); - break; + default: + throw new Exception("'{$arch}' enum not found."); + break; } } @@ -113,19 +155,393 @@ static public function isPPC(): bool static public function isArch(string $arch): bool { switch ($arch) { - case self::X86: - return self::isX86(); - break; - case self::PPC: - return self::isPPC(); - break; - case self::ARM: - return self::isArm(); - break; + case self::X86: + return self::isX86(); + break; + case self::PPC: + return self::isPPC(); + break; + case self::ARM: + return self::isArm(); + break; + + default: + throw new Exception("'{$arch}' not found."); + break; + } + } + + /** + * Gets the system's total amount of CPU cores. + * + * @return int + * + * @throws Exception + */ + static public function getCPUCores(): int + { + switch (self::getOS()) { + case 'Linux': + $cpuinfo = file_get_contents('/proc/cpuinfo'); + preg_match_all('/^processor/m', $cpuinfo, $matches); + return count($matches[0]); + case 'Darwin': + return intval(shell_exec('sysctl -n hw.ncpu')); + case 'Windows': + return intval(shell_exec('wmic cpu get NumberOfCores')); + default: + throw new Exception(self::getOS() . " not supported."); + } + } + + /** + * Helper function to read a Linux System's /proc/stat data and convert it into an array. + * + * @return array + */ + static private function getProcStatData(): array + { + $data = []; + + $totalCPUExists = false; + + $cpustats = file_get_contents('/proc/stat'); + + $cpus = explode("\n", $cpustats); + + // Remove non-CPU lines + $cpus = array_filter($cpus, function ($cpu) { + return preg_match('/^cpu[0-999]/', $cpu); + }); + + foreach ($cpus as $cpu) { + $cpu = explode(' ', $cpu); + + // get CPU number + $cpuNumber = substr($cpu[0], 3); + + if ($cpu[0] === 'cpu') { + $totalCPUExists = true; + $cpuNumber = 'total'; + } + + $data[$cpuNumber]['user'] = $cpu[1] ?? 0; + $data[$cpuNumber]['nice'] = $cpu[2] ?? 0; + $data[$cpuNumber]['system'] = $cpu[3] ?? 0; + $data[$cpuNumber]['idle'] = $cpu[4] ?? 0; + $data[$cpuNumber]['iowait'] = $cpu[5] ?? 0; + $data[$cpuNumber]['irq'] = $cpu[6] ?? 0; + $data[$cpuNumber]['softirq'] = $cpu[7] ?? 0; + // These might not exist on older kernels. + $data[$cpuNumber]['steal'] = $cpu[8] ?? 0; + $data[$cpuNumber]['guest'] = $cpu[9] ?? 0; + } + + if (!$totalCPUExists) { + // Combine all values + $data['total'] = [ + 'user' => 0, + 'nice' => 0, + 'system' => 0, + 'idle' => 0, + 'iowait' => 0, + 'irq' => 0, + 'softirq' => 0, + 'steal' => 0, + 'guest' => 0 + ]; + + foreach ($data as $cpu) { + $data['total']['user'] += intval($cpu['user']); + $data['total']['nice'] += intval($cpu['nice'] ?? 0); + $data['total']['system'] += intval($cpu['system'] ?? 0); + $data['total']['idle'] += intval($cpu['idle'] ?? 0); + $data['total']['iowait'] += intval($cpu['iowait'] ?? 0); + $data['total']['irq'] += intval($cpu['irq'] ?? 0); + $data['total']['softirq'] += intval($cpu['softirq'] ?? 0); + $data['total']['steal'] += intval($cpu['steal'] ?? 0); + $data['total']['guest'] += intval($cpu['guest'] ?? 0); + } + } + + return $data; + } + + /** + * Gets the current usage of a core as a percentage. Passing 0 will return the usage of all cores combined. + * + * @param int $core + * + * @return int + * + * @throws Exception + */ + static public function getCPUUtilisation(int $id = 0): int + { + switch (self::getOS()) { + case 'Linux': + $cpuNow = self::getProcStatData(); + $i = 0; + + $data = []; + + foreach ($cpuNow as $cpu) { + // Check if this is the total CPU + $cpuTotal = $cpu['user'] + $cpu['nice'] + $cpu['system'] + $cpu['idle'] + $cpu['iowait'] + $cpu['irq'] + $cpu['softirq'] + $cpu['steal']; + + $cpuIdle = $cpu['idle']; + + $idleDelta = $cpuIdle - (isset($lastData[$i]) ? $lastData[$i]['idle'] : 0); + + $totalDelta = $cpuTotal - (isset($lastData[$i]) ? $lastData[$i]['total'] : 0); + + $lastData[$i]['total'] = $cpuTotal; + $lastData[$i]['idle'] = $cpuIdle; + + $result = (1.0 - ($idleDelta / $totalDelta)) * 100; + + $data[$i] = $result; + + $i++; + } + + if ($id === 0) { + return intval(array_sum($data)); + } else { + return $data[$id]; + } default: - throw new Exception("'{$arch}' not found."); - break; + throw new Exception(self::getOS() . " not supported."); } } + + /** + * Returns the total amount of RAM available on the system as Megabytes. + * + * @return int + * + * @throws Exception + */ + static public function getMemoryTotal(): int + { + switch (self::getOS()) { + case 'Linux': + $meminfo = file_get_contents('/proc/meminfo'); + preg_match('/MemTotal:\s+(\d+)/', $meminfo, $matches); + + if (isset($matches[1])) { + return intval(intval($matches[1]) / 1024); + } else { + throw new Exception('Could not find MemTotal in /proc/meminfo.'); + } + break; + case 'Darwin': + return intval((intval(shell_exec('sysctl -n hw.memsize'))) / 1024 / 1024); + break; + default: + throw new Exception(self::getOS() . " not supported."); + } + } + + /** + * Returns the total amount of Free RAM available on the system as Megabytes. + * + * @return int + * + * @throws Exception + */ + static public function getMemoryFree(): int + { + switch (self::getOS()) { + case 'Linux': + $meminfo = file_get_contents('/proc/meminfo'); + preg_match('/MemFree:\s+(\d+)/', $meminfo, $matches); + if (isset($matches[1])) { + return intval(intval($matches[1]) / 1024); + } else { + throw new Exception('Could not find MemFree in /proc/meminfo.'); + } + case 'Darwin': + return intval(intval(shell_exec('sysctl -n vm.page_free_count')) / 1024 / 1024); + default: + throw new Exception(self::getOS() . " not supported."); + } + } + + /** + * Returns the total amount of Disk space on the system as Megabytes. + * + * @return int + * + * @throws Exception + */ + static public function getDiskTotal(): int + { + $totalSpace = disk_total_space(__DIR__); + + if ($totalSpace === false) { + throw new Exception('Unable to get disk space'); + } + + return intval($totalSpace / 1024 / 1024); + } + + /** + * Returns the total amount of Disk space free on the system as Megabytes. + * + * @return int + * + * @throws Exception + */ + static public function getDiskFree(): int + { + $totalSpace = disk_free_space(__DIR__); + + if ($totalSpace === false) { + throw new Exception('Unable to get free disk space'); + } + + return intval($totalSpace / 1024 / 1024); + } + + /** + * Helper function to read a Linux System's /proc/diskstats data and convert it into an array. + * + * @return array + */ + static private function getDiskStats() + { + // Read /proc/diskstats + $diskstats = file_get_contents('/proc/diskstats'); + + // Split the data + $diskstats = explode("\n", $diskstats); + + // Remove excess spaces + $diskstats = array_map(function ($data) { + return preg_replace('/\t+/', ' ', trim($data)); + }, $diskstats); + + // Remove empty lines + $diskstats = array_filter($diskstats, function ($data) { + return !empty($data); + }); + + $data = []; + foreach ($diskstats as $disk) { + // Breakdown the data + $disk = explode(' ', $disk); + + $data[$disk[2]] = $disk; + } + + return $data; + } + + /** + * Returns an array of all the available storage devices on the system containing + * the current read and write usage in Megabytes. + * There is also a ['total'] key that contains the total amount of read and write usage. + * + * @param int $duration + * @return array + * + * @throws Exception + */ + static public function getIOUsage($duration = 1): array + { + $diskStat = self::getDiskStats(); + sleep($duration); + $diskStat2 = self::getDiskStats(); + + // Remove invalid disks + $diskStat = array_filter($diskStat, function ($disk) { + foreach (self::INVALIDDISKS as $filter) { + if (!isset($disk[2])) { + return false; + } + if (str_contains($disk[2], $filter)) { + return false; + } + } + + return true; + }); + + $diskStat2 = array_filter($diskStat2, function ($disk) { + foreach (self::INVALIDDISKS as $filter) { + if (!isset($disk[2])) { + return false; + } + + if (str_contains($disk[2], $filter)) { + return false; + } + } + + return true; + }); + + $stats = []; + + // Compute Delta + foreach ($diskStat as $key => $disk) { + $stats[$key]['read'] = (((intval($diskStat2[$key][5]) - intval($disk[5])) * 512) / 1048576); + $stats[$key]['write'] = (((intval($diskStat2[$key][9]) - intval($disk[9])) * 512) / 1048576); + } + + $stats['total']['read'] = array_sum(array_column($stats, 'read')); + $stats['total']['write'] = array_sum(array_column($stats, 'write')); + + return $stats; + } + + /** + * Returns an array of all the available network interfaces on the system + * containing the current download and upload usage in Megabytes. + * There is also a ['total'] key that contains the total amount of download + * and upload + * + * @param int $duration The buffer duration to fetch the data points + * + * @return array + * + * @throws Exception + */ + static public function getNetworkUsage($duration = 1): array + { + // Create a list of interfaces + $interfaces = scandir('/sys/class/net', SCANDIR_SORT_NONE); + + // Remove all unwanted interfaces + $interfaces = array_filter($interfaces, function ($interface) { + foreach (self::INVALIDNETINTERFACES as $filter) { + if (str_contains($interface, $filter)) { + return false; + } + } + + return true; + }); + + // Get the total IO Usage + $IOUsage = []; + + foreach ($interfaces as $interface) { + $tx1 = intval(file_get_contents('/sys/class/net/' . $interface . '/statistics/tx_bytes')); + $rx1 = intval(file_get_contents('/sys/class/net/' . $interface . '/statistics/rx_bytes')); + sleep($duration); + $tx2 = intval(file_get_contents('/sys/class/net/' . $interface . '/statistics/tx_bytes')); + $rx2 = intval(file_get_contents('/sys/class/net/' . $interface . '/statistics/rx_bytes')); + + $IOUsage[$interface]['download'] = round(($rx2 - $rx1) / 1048576, 2); + $IOUsage[$interface]['upload'] = round(($tx2 - $tx1) / 1048576, 2); + } + + $IOUsage['total']['download'] = array_sum(array_column($IOUsage, 'download')); + $IOUsage['total']['upload'] = array_sum(array_column($IOUsage, 'upload')); + + return $IOUsage; + } } diff --git a/tests/System/SystemTest.php b/tests/System/SystemTest.php index 8504ae1..0bf80a9 100644 --- a/tests/System/SystemTest.php +++ b/tests/System/SystemTest.php @@ -41,4 +41,69 @@ public function testOs() $this->expectException("Exception"); System::isArch("throw"); } + + public function testGetCPUCores() + { + $this->assertIsInt(System::getCPUCores()); + } + + public function testGetDiskTotal() + { + $this->assertIsInt(System::getDiskTotal()); + } + + public function testGetDiskFree() + { + $this->assertIsInt(System::getDiskFree()); + } + + // Methods only implemented for Linux + public function testGetCPUUtilisation() + { + if (System::getOS() === 'Linux') { + $this->assertIsInt(System::getCPUUtilisation()); + } else { + $this->expectException("Exception"); + System::getCPUUtilisation(); + } + } + + public function testGetMemoryTotal() + { + if (System::getOS() === 'Linux') { + $this->assertIsInt(System::getMemoryTotal()); + } else { + $this->expectException("Exception"); + System::getMemoryTotal(); + } + } + + public function testGetMemoryFree() + { + if (System::getOS() === 'Linux') { + $this->assertIsInt(System::getMemoryFree()); + } else { + $this->expectException("Exception"); + System::getMemoryFree(); + } + } + + public function testGetIOUsage() + { + if (System::getOS() === 'Linux') { + $this->assertIsArray(System::getIOUsage()); + } else { + $this->expectException("Exception"); + System::getIOUsage(); + } + } + + public function testGetNetworkUsage() { + if (System::getOS() === 'Linux') { + $this->assertIsArray(System::getNetworkUsage()); + } else { + $this->expectException("Exception"); + System::getNetworkUsage(); + } + } }