summaryrefslogtreecommitdiffstats
path: root/modules-available/systemstatus/inc/systemstatus.inc.php
blob: c50e0ef6d59f8390e0c22901f9bcf8de214e37d9 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
<?php

class SystemStatus
{

	const PROP_UPGRADABLE_COUNT = 'systemstatus.upgradable-count';

	/**
	 * Collect status about the disk and vmstore.
	 * The *Usage vars are filled with arrays with keys mountPoint, fileSystem,
	 * usedPercent, sizeKb, freeKb.
	 * @param array|false $systemUsage
	 * @param array|false $storeUsage
	 * @param string|false $currentSource What's currently mounted as vmstore
	 * @param string|false $wantedSource What should be mounted as vmstore (false if nothing configured)
	 * @return bool false if querying fs data from taskmanager failed
	 */
	public static function diskStat(&$systemUsage, &$storeUsage, &$currentSource = false, &$wantedSource = false): bool
	{
		$task = Taskmanager::submit('DiskStat');
		if ($task === false)
			return false;
		$task = Taskmanager::waitComplete($task, 3000);

		if (empty($task['data']['list']))
			return false;
		$wantedSource = Property::getVmStoreUrl();
		$storeUsage = false;
		$systemUsage = false;
		if ($wantedSource === '<local>') {
			$storePoint = '/';
			$currentSource = $wantedSource;
		} else {
			$storePoint = CONFIG_VMSTORE_DIR;
			$currentSource = false;
		}
		// Collect free space information
		foreach ($task['data']['list'] as $entry) {
			// StorePoint is either the actual directory of the vmstore, or / if we use internal storage
			if ($entry['mountPoint'] === $storePoint) {
				$storeUsage = $entry;
			}
			// Always report free space on system disk
			if ($entry['mountPoint'] === '/') {
				$systemUsage = $entry;
			}
			// Record what's mounted at destination, regardless of config, to indicate something is wrong
			if ($entry['mountPoint'] === CONFIG_VMSTORE_DIR) {
				$currentSource = $entry['fileSystem'];

				// If internal/local storage is used but there is a mount on CONFIG_VMSTORE_DIR,
				// we assume it's on purpose like a second hdd and use that
				if ($wantedSource === '<local>') {
					$wantedSource = $currentSource;
					$storeUsage = $entry;
				}
			}
		}
		return true;
	}

	/**
	 * Get timestamp of the available updates. This is an estimate; the downloaded apt data usually
	 * preserves the Last-Modified timestamp from the HTTP download of the according data. Note
	 * that this list gets updated whenever any available package changes, so it does not necessarily
	 * mean that any currently installed package can be updated when the list changes.
	 */
	public static function getAptLastDbUpdateTime(): int
	{
		$osRelease = parse_ini_file('/etc/os-release');
		$updateDbTime = 0;
		foreach (glob('/var/lib/apt/lists/*_dists_' . ($osRelease['VERSION_CODENAME'] ?? '') . '*_InRelease', GLOB_NOSORT) as $f) {
			$b = basename($f);
			if (preg_match('/dists_[a-z]+(?:[\-_](?:updates|security))?_InRelease$/', $b)) {
				$updateDbTime = max($updateDbTime, filemtime($f));
			}
		}
		return $updateDbTime;
	}

	/**
	 * Get timestamp when the apt database was last attempted to be updated. This does not
	 * imply that the operation was successful.
	 */
	public static function getAptLastUpdateAttemptTime(): int
	{
		return (int)filemtime('/var/lib/apt/lists/partial');
	}

	/**
	 * Get when the dpkg database was last changed, i.e. when a package was last installed, updated or removed.
	 * This is an estimate as it just looks at the modification time of relevant files. It is possible these
	 * files get modified for other reasons.
	 */
	public static function getDpkgLastPackageChanges(): int
	{
		return (int)filemtime(file_exists('/var/log/dpkg.log') ? '/var/log/dpkg.log' : '/var/lib/dpkg/status');
	}

	/**
	 * Get list of packages that have been updated, but require a reboot of the system
	 * to fully take effect.
	 * @return string[]
	 */
	public static function getPackagesRequiringReboot(): array
	{
		if (!file_exists('/run/reboot-required.pkgs'))
			return [];
		$lines = file('/run/reboot-required.pkgs', FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
		return array_unique($lines);
	}

	/**
	 * Get a task struct querying upgradable packages. This adds
	 * a hook to the task to cache the number of upgradable packages
	 * that are security upgrades, so it is preferred to call this
	 * wrapper instead of running the task directly.
	 */
	public static function getUpgradableTask()
	{
		$task = Taskmanager::submit('AptGetUpgradable');
		if (Taskmanager::isTask($task)) {
			TaskmanagerCallback::addCallback($task, 'ssUpgradable');
		}
		return $task;
	}

	/**
	 * Called from Taskmanager callback after invoking getUpgradableTask().
	 */
	public static function setUpgradableData(array $task)
	{
		if (Taskmanager::isFailed($task) || !Taskmanager::isFinished($task) || !isset($task['data']['packages']))
			return;
		$count = 0;
		foreach ($task['data']['packages'] as $package) {
			if (substr($package['source'], -9) === '-security') {
				$count++;
			}
		}
		error_log("Upgradable security count: $count, total count: " . count($task['data']['packages']));
		Property::set(self::PROP_UPGRADABLE_COUNT, $count . '|' . count($task['data']['packages']), 61);
	}

	/**
	 * Get number of packages that can be upgraded and are security updates.
	 * This is a cached value and should be updated at least once an hour.
	 */
	public static function getUpgradableSecurityCount(): int
	{
		$str = Property::get(self::PROP_UPGRADABLE_COUNT);
		if ($str === false)
			return 0;
		$p = explode('|', $str);
		if (empty($p) || !is_numeric($p[0]))
			return 0;
		return (int)$p[0];
	}

}