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
|
<?php
class RemoteAccess
{
const PROP_ALLOWED_VNC_NET = 'remoteaccess.allowedvncaccess';
const PROP_TRY_VIRT_HANDOVER = 'remoteaccess.virthandover';
const PROP_VNC_PORT = 'remoteaccess.vncport';
const PROP_PLUGIN_VERSION = 'remoteaccess.plugin-version';
/** @var int How long we wait for a client to boot up, i.e. assume it will become online soon */
const WOL_TIMEOUT = 120;
/**
* Get a list of locationIds where remote access is enabled. If $filterOverridden is true,
* the list will not contain any locations where remote access is disabled via location override.
* @param int $group Group to get locations for, or '0' for all locations
* @param bool $filterOverridden iff true, remove any locations where remote access is currently disabled
*/
public static function getEnabledLocations(int $group = 0, bool $filterOverridden = true): array
{
if ($group === 0) {
$list = Database::queryColumnArray("SELECT DISTINCT rxl.locationid FROM remoteaccess_x_location rxl
INNER JOIN remoteaccess_group g ON (g.groupid = rxl.groupid AND g.active = 1)");
} else {
$list = Database::queryColumnArray("SELECT DISTINCT locationid FROM remoteaccess_x_location
WHERE groupid = :gid", ['gid' => $group]);
}
if (!$filterOverridden || !Module::isAvailable('rebootcontrol'))
return $list;
return array_filter($list, function (int $lid) {
$mode = Scheduler::getLocationOptions($lid)['ra-mode'];
return ($mode !== Scheduler::RA_NEVER
&& ($mode !== Scheduler::RA_SELECTIVE || !OpeningTimes::isRoomOpen($lid, 5, 5)));
});
}
/**
* Tries to ensure that the requested amount of machines is running for every remote access group.
*/
public static function ensureMachinesRunning(): void
{
if (!Module::isAvailable('rebootcontrol')) {
error_log("Not waking remote access machines: rebootcontrol missing");
return;
}
self::updateFailCounters();
$res = Database::simpleQuery("SELECT rg.groupid, rg.groupname, rg.wolcount,
GROUP_CONCAT(rxl.locationid) AS locs
FROM remoteaccess_group rg
INNER JOIN remoteaccess_x_location rxl USING (groupid)
WHERE rg.active = 1
GROUP BY groupid");
// Consider machines we tried to wake in the past 120 seconds as online
$wolDeadline = time() - self::WOL_TIMEOUT;
foreach ($res as $row) {
$wantNum = $row['wolcount'];
// This can't really be anything but a CSV list, but better be safe
$locs = preg_replace('/[^0-9,]/', '', $row['locs']);
if (!empty($locs)) {
// Filter out locations for which remote-access is disabled
$locArray = explode(',', $locs);
$locArray = array_filter($locArray, function (int $lid) {
$mode = Scheduler::getLocationOptions($lid)['ra-mode'];
return ($mode !== Scheduler::RA_NEVER
&& ($mode !== Scheduler::RA_SELECTIVE || !OpeningTimes::isRoomOpen($lid, 5, 5)));
});
$locs = implode(',', $locArray);
}
if ($wantNum > 0 && !empty($locs)) {
$active = Database::queryFirst("SELECT Count(*) AS cnt FROM machine m
INNER JOIN remoteaccess_machine rm USING (machineuuid)
WHERE m.locationid IN ($locs) AND (m.state = 'IDLE' OR rm.woltime > $wolDeadline)");
$active = ($active['cnt'] ?? 0);
$wantNum -= $active;
}
if ($wantNum > 0) {
$numFailed = self::tryWakeMachines($locs, $wantNum);
} else {
$numFailed = 0;
}
Database::exec("UPDATE remoteaccess_group SET unwoken = :num WHERE groupid = :groupid",
['num' => $numFailed, 'groupid' => $row['groupid']]);
}
}
/**
* Tries to wake machines based on provided locations and a given number limit.
* @param string $locs The locations for which machines need to be woken up.
* @param int $num The maximum number of machines to wake up.
* @return int The remaining number of machines that could not be woken up.
*/
private static function tryWakeMachines(string $locs, int $num): int
{
if (empty($locs))
return $num;
$NOW = time();
$res = Database::simpleQuery("SELECT m.machineuuid, m.macaddr, m.clientip, m.locationid FROM machine m
LEFT JOIN remoteaccess_machine rm USING (machineuuid)
WHERE m.locationid IN ($locs)
AND m.state IN ('OFFLINE', 'STANDBY')
AND rm.woltime < :woldeadline AND rm.wolfails < 10
ORDER BY rm.wolfails ASC, rm.woltime ASC
LIMIT 100", ['woldeadline' => $NOW - 300]);
while ($num > 0) {
$list = [];
for ($i = 0; $i < $num && ($row = $res->fetch()); ++$i) {
$list[] = $row;
Database::exec("INSERT INTO remoteaccess_machine (machineuuid, password, woltime)
VALUES (:uuid, NULL, :now)
ON DUPLICATE KEY UPDATE woltime = VALUES(woltime)",
['uuid' => $row['machineuuid'], 'now' => $NOW]);
}
if (empty($list))
break; // No more clients in this location
RebootControl::wakeMachines($list, $fails);
$num -= (count($list) - count($fails));
if (!empty($fails)) {
$failIds = ArrayUtil::flattenByKey($fails, 'machineuuid');
// Reduce time so they won't be marked as wol_in_progress
Database::exec('UPDATE remoteaccess_machine SET woltime = :faketime WHERE machineuuid IN (:fails)',
['faketime' => $NOW - self::WOL_TIMEOUT, 'fails' => $failIds]);
}
}
return $num;
}
/**
* Update the fail counters for remote access machines that didn't turn on.
* We assume a WoL-attempt failed if it took longer than two minutes and
* the machine is still not up.
*/
private static function updateFailCounters(): void
{
$upper = time() - self::WOL_TIMEOUT;
$lower = $upper - self::WOL_TIMEOUT;
Database::exec("UPDATE remoteaccess_machine rm, machine m
SET rm.wolfails = rm.wolfails + 1, rm.woltime = rm.woltime - :woltimeout
WHERE (rm.woltime BETWEEN :lower AND :upper) AND rm.machineuuid = m.machineuuid
AND m.state IN ('OFFLINE', 'STANDBY') AND m.lastseen < :lower", [
'lower' => $lower,
'upper' => $upper,
'woltimeout' => self::WOL_TIMEOUT,
]);
}
}
|