Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions css/main-CIYKh9bo.chunk.css

Large diffs are not rendered by default.

1 change: 0 additions & 1 deletion css/main-CaivFkOz.chunk.css

This file was deleted.

2 changes: 1 addition & 1 deletion css/serverinfo-main.css
Original file line number Diff line number Diff line change
@@ -1,2 +1,2 @@
/* extracted by css-entry-points-plugin */
@import './main-CaivFkOz.chunk.css';
@import './main-CIYKh9bo.chunk.css';
26 changes: 13 additions & 13 deletions js/serverinfo-main.mjs

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion js/serverinfo-main.mjs.map

Large diffs are not rendered by default.

16 changes: 16 additions & 0 deletions lib/Config/ConfigLexicon.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@
class ConfigLexicon implements ILexicon {
public const CACHED_SLOWEST_JOBS = 'cached_slowest_jobs';
public const JOB_INTERVAL_JOB_STATS = 'job_interval_job_stats';
public const CACHED_BOOT_TIME = 'cached_boot_time';
public const CACHED_BOOT_TIME_SAMPLED_AT = 'cached_boot_time_sampled_at';

#[\Override]
public function getStrictness(): Strictness {
Expand All @@ -39,6 +41,20 @@ public function getAppConfigs(): array {
'Slowest background jobs, as collected by OCA\ServerInfo\Jobs\UpdateJobStats',
lazy: true,
),
new Entry(
self::CACHED_BOOT_TIME,
ValueType::INT,
0,
'Boot time as a unix timestamp, remembered so that uptime does not cost a process fork on every poll',
lazy: true,
),
new Entry(
self::CACHED_BOOT_TIME_SAMPLED_AT,
ValueType::INT,
0,
'When the boot time above was last measured',
lazy: true,
),
new Entry(
self::JOB_INTERVAL_JOB_STATS,
ValueType::INT,
Expand Down
11 changes: 8 additions & 3 deletions lib/DatabaseStatistics.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ public function __construct(IConfig $config, IDBConnection $connection) {
}

/**
* @return array{type: string, version: string, size: string}
* @return array{type: string, version: string, size: int}
*/
public function getDatabaseStatistics(): array {
return [
Expand Down Expand Up @@ -67,7 +67,10 @@ protected function databaseVersion(): string {
* @copyright (c) phpBB Limited <https://www.phpbb.com>
* @license GNU General Public License, version 2 (GPL-2.0)
*/
protected function databaseSize(): string {
/**
* @return int size in bytes, or -1 when it cannot be determined
*/
protected function databaseSize(): int {
$database_size = false;
// This code is heavily influenced by a similar routine in phpMyAdmin 2.2.0
switch ($this->config->getSystemValue('dbtype')) {
Expand Down Expand Up @@ -110,7 +113,9 @@ protected function databaseSize(): string {
$result->closeCursor();
break;
}
return ($database_size !== false) ? (string)$database_size : 'N/A';
// -1 rather than the string 'N/A': the size is consumed as a number, and a
// string reaches the UI as "NaN B".
return ($database_size !== false) ? (int)$database_size : -1;
}

/**
Expand Down
3 changes: 1 addition & 2 deletions lib/LiveData.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,12 +17,11 @@ public function __construct(
}

public function getData(): array {
$cpu = $this->os->getCPU();
$memory = $this->os->getMemory();

return [
'cpu' => [
'load' => $cpu->getAverageLoad(),
'load' => $this->os->getAverageLoad(),
],
'memory' => [
'total' => $memory->getMemTotal(),
Expand Down
9 changes: 8 additions & 1 deletion lib/OperatingSystems/Dummy.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,14 @@ public function supported(): bool {

#[\Override]
public function getCPU(): CPU {
return new CPU('Unknown Processor', 1);
// -1, not 1: a fabricated core count is used as a divisor for the load
// average and would report a single-core machine at 800% busy.
return new CPU('Unknown Processor', -1);
}

#[\Override]
public function sampleUptime(): int {
return -1;
}

#[\Override]
Expand Down
8 changes: 7 additions & 1 deletion lib/OperatingSystems/FreeBSD.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,11 +30,17 @@ public function getCPU(): CPU {
$cores = (int)$this->executeCommand('/sbin/sysctl -n kern.smp.cpus');
} catch (RuntimeException) {
$name = 'Unknown Processor';
$cores = 1;
// -1 marks the count as unknown rather than claiming a single core.
$cores = -1;
}
return new CPU($name, $cores);
}

#[\Override]
public function sampleUptime(): int {
return -1;
}

#[\Override]
public function getMemory(): Memory {
$data = new Memory();
Expand Down
8 changes: 8 additions & 0 deletions lib/OperatingSystems/IOperatingSystem.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,14 @@ public function supported(): bool;

public function getCPU(): CPU;

/**
* Uptime obtained by a means that may cost a process fork, used only when
* getUptime() cannot answer.
*
* @return int uptime in seconds, or -1 when it cannot be determined
*/
public function sampleUptime(): int;

public function getMemory(): Memory;

/**
Expand Down
186 changes: 176 additions & 10 deletions lib/OperatingSystems/Linux.php
Original file line number Diff line number Diff line change
Expand Up @@ -30,12 +30,12 @@ public function supported(): bool {

#[\Override]
public function getCPU(): CPU {
$default = new CPU('Unknown Processor', 1);

try {
$cpuinfo = $this->readContent('/proc/cpuinfo');
} catch (RuntimeException) {
return $default;
// -1 threads means "unknown"; a fabricated 1 would be used as the
// divisor for the load average and turn a 2-core box into "800 %".
return new CPU('Unknown Processor', $this->countProcessors());
}

$matches = [];
Expand All @@ -48,12 +48,128 @@ public function getCPU(): CPU {
$pattern = '/model name\s:\s(.+)/';
}

// Counted separately from the model name: aarch64 and s390x list every
// processor but carry no "model name" line at all.
$threads = substr_count($cpuinfo, "processor\t");
if ($threads < 1) {
$threads = $this->countProcessors();
}

$result = preg_match_all($pattern, $cpuinfo, $matches);
if ($result === 0 || $result === false) {
return $default;
return new CPU('Unknown Processor', $threads);
}

return new CPU($matches[1][0], $threads);
}

/**
* Read memory from free(1) when /proc/meminfo is out of reach.
*
* {@see countProcessors()} for why a web server may be able to run commands
* but not read /proc.
*/
private function getMemoryFromFree(): Memory {
$data = new Memory();

try {
$output = $this->executeCommand('free -k');
} catch (RuntimeException) {
return $data;
}

// total used free shared buff/cache available. The last three columns are
// missing on older procps, so only the first three are required.
if (preg_match('/^Mem:\s+(\d+)\s+(\d+)\s+(\d+)(?:\s+\d+\s+\d+\s+(\d+))?/m', $output, $matches) === 1) {
$data->setMemTotal((int)((int)$matches[1] / 1024));
$data->setMemFree((int)((int)$matches[3] / 1024));
// Without an "available" column, free is the closest honest answer.
$data->setMemAvailable((int)((int)($matches[4] ?? $matches[3]) / 1024));
}

if (preg_match('/^Swap:\s+(\d+)\s+(\d+)\s+(\d+)/m', $output, $matches) === 1) {
$data->setSwapTotal((int)((int)$matches[1] / 1024));
$data->setSwapFree((int)((int)$matches[3] / 1024));
}

return $data;
}

/**
* Read memory from the per-NUMA-node files under /sys.
*
* ProcSubset only hides /proc, so these survive on a web server that cannot
* read /proc/meminfo. They carry no swap totals and no MemAvailable, so swap
* stays unknown and available is approximated the way the kernel does it:
* free memory plus the page cache and slab it can reclaim.
*/
private function getMemoryFromSys(): Memory {
$data = new Memory();

$total = 0;
$free = 0;
$reclaimable = 0;
$found = false;

foreach ($this->getMemoryNodes() as $node) {
try {
$content = $this->readContent($node);
} catch (RuntimeException) {
continue;
}

$found = true;
$total += $this->readNodeMemValue($content, 'MemTotal');
$free += $this->readNodeMemValue($content, 'MemFree');
$reclaimable += $this->readNodeMemValue($content, 'Inactive(file)')
+ $this->readNodeMemValue($content, 'SReclaimable');
}

if (!$found || $total <= 0) {
return $data;
}

$data->setMemTotal((int)($total / 1024));
$data->setMemFree((int)($free / 1024));
$data->setMemAvailable((int)(($free + $reclaimable) / 1024));

return $data;
}

/**
* @return string[] paths of the per-node meminfo files
*/
protected function getMemoryNodes(): array {
return glob('/sys/devices/system/node/node*/meminfo') ?: [];
}

/**
* @return int the value in kB, or 0 when the key is absent
*/
private function readNodeMemValue(string $content, string $key): int {
$pattern = '/^Node \d+ ' . preg_quote($key, '/') . ':\s+(\d+) kB$/m';

return preg_match($pattern, $content, $matches) === 1 ? (int)$matches[1] : 0;
}

/**
* Ask the system for its processor count.
*
* Debian's and Ubuntu's apache2 unit ships ProcSubset=pid, which hides
* everything but per-process directories in /proc from the web server while
* leaving commands runnable. nproc then still knows what /proc/cpuinfo
* cannot tell us.
*
* @return int the number of processors, or -1 when it cannot be determined
*/
private function countProcessors(): int {
try {
$output = trim($this->executeCommand('nproc'));
} catch (RuntimeException) {
return -1;
}

return new CPU($matches[1][0], substr_count($cpuinfo, "processor\t"));
return preg_match('/^\d+$/', $output) === 1 ? (int)$output : -1;
}

#[\Override]
Expand All @@ -63,7 +179,13 @@ public function getMemory(): Memory {
try {
$meminfo = $this->readContent('/proc/meminfo');
} catch (RuntimeException $e) {
return $data;
// /sys is tried first because it settles which situation we are in
// without paying for a fork: where it can be read, /proc is hidden by
// ProcSubset and free(1) — which reads /proc/meminfo itself — is bound
// to fail as well. Where it cannot (open_basedir), free still can.
$fromSys = $this->getMemoryFromSys();

return $fromSys->getMemTotal() > 0 ? $fromSys : $this->getMemoryFromFree();
}

$matches = [];
Expand Down Expand Up @@ -241,11 +363,23 @@ private function bytesToMebibytes(int $bytes): int {

#[\Override]
public function getTime(): string {
try {
return $this->executeCommand('date');
} catch (RuntimeException $e) {
return '';
// date(1) used to be forked here on every two-second poll for something
// PHP already knows. Nextcloud runs on UTC internally, so the system zone
// is read from /etc/localtime to keep reporting the server's own clock.
return (new \DateTimeImmutable('now', $this->getSystemTimeZone()))->format('D M j H:i:s T Y');
}

private function getSystemTimeZone(): \DateTimeZone {
$link = @readlink('/etc/localtime');
if (is_string($link) && preg_match('#/zoneinfo/(.+)$#', $link, $matches) === 1 && $matches[1] !== '') {
try {
return new \DateTimeZone($matches[1]);
} catch (\Exception) {
// fall through to the configured default
}
}

return new \DateTimeZone(date_default_timezone_get());
}

#[\Override]
Expand All @@ -263,6 +397,38 @@ public function getUptime(): int {
return $uptimeInSeconds;
}

/**
* A process that has only just started carries the system uptime in its own
* /proc/<pid>/stat, and a process may always read its own entry — which is
* what makes this work where /proc/uptime itself is hidden. Field 22 is the
* start time in clock ticks since boot, and USER_HZ is fixed at 100 for the
* /proc ABI regardless of the kernel's own HZ.
*
* Costs a fork, so {@see Os::getUptime()} only asks occasionally.
*/
#[\Override]
public function sampleUptime(): int {
try {
$stat = $this->executeCommand('cat /proc/self/stat');
} catch (RuntimeException) {
return -1;
}

// The second field is the executable name in brackets and may itself
// contain spaces, so the fields are counted from the closing bracket.
$close = strrpos($stat, ')');
if ($close === false) {
return -1;
}

$fields = preg_split('/\s+/', trim(substr($stat, $close + 2))) ?: [];
if (!isset($fields[19]) || preg_match('/^\d+$/', $fields[19]) !== 1) {
return -1;
}

return intdiv((int)$fields[19], 100);
}

#[\Override]
public function getNetworkInfo(): array {
$result = [
Expand Down
Loading
Loading