129 lines
3.9 KiB
PHP
129 lines
3.9 KiB
PHP
<?php
|
|
|
|
namespace App\Services\Deployment;
|
|
|
|
use App\Support\Settings;
|
|
use Illuminate\Support\Carbon;
|
|
|
|
/**
|
|
* When the installation is allowed to update itself.
|
|
*
|
|
* The button stays: manual is always possible, at any hour. This is the other
|
|
* option — tick it, name a window, and the installation takes the update in
|
|
* that window on its own. An update is one to three minutes of maintenance
|
|
* mode, which is short enough not to announce and long enough that nobody
|
|
* wants it at eleven in the morning.
|
|
*
|
|
* There is nothing to guard against having already updated by hand: an
|
|
* installation that is current has nothing available, and the window finds
|
|
* nothing to do. The condition is "is there something newer", not "has today's
|
|
* update run".
|
|
*/
|
|
class UpdateWindow
|
|
{
|
|
public const KEY_ENABLED = 'deploy.auto_update';
|
|
|
|
public const KEY_DAYS = 'deploy.auto_days';
|
|
|
|
public const KEY_FROM = 'deploy.auto_from';
|
|
|
|
public const KEY_TO = 'deploy.auto_to';
|
|
|
|
/**
|
|
* Tuesday to Thursday by default.
|
|
*
|
|
* Not Monday — the week starts and every fault costs double. Not Friday —
|
|
* nobody wants to repair a deployment on a Friday afternoon. ISO weekdays,
|
|
* so 1 is Monday.
|
|
*/
|
|
public const DEFAULT_DAYS = [2, 3, 4];
|
|
|
|
/**
|
|
* Half past five by default.
|
|
*
|
|
* Not three in the morning: if it goes wrong, the person who has to fix it
|
|
* is asleep, and an outage nobody is awake for is an outage that lasts
|
|
* until somebody wakes up.
|
|
*/
|
|
public const DEFAULT_FROM = '05:00';
|
|
|
|
public const DEFAULT_TO = '06:00';
|
|
|
|
public function enabled(): bool
|
|
{
|
|
return Settings::bool(self::KEY_ENABLED, false);
|
|
}
|
|
|
|
/** @return array<int, int> ISO weekdays */
|
|
public function days(): array
|
|
{
|
|
$days = Settings::get(self::KEY_DAYS, self::DEFAULT_DAYS);
|
|
|
|
if (! is_array($days)) {
|
|
return self::DEFAULT_DAYS;
|
|
}
|
|
|
|
// Filtered rather than trusted: this is read on a schedule, and a
|
|
// malformed setting must not be able to mean "every day" by accident.
|
|
$days = array_values(array_unique(array_filter(
|
|
array_map('intval', $days),
|
|
fn (int $day) => $day >= 1 && $day <= 7,
|
|
)));
|
|
|
|
return $days === [] ? self::DEFAULT_DAYS : $days;
|
|
}
|
|
|
|
public function from(): string
|
|
{
|
|
return $this->time(self::KEY_FROM, self::DEFAULT_FROM);
|
|
}
|
|
|
|
public function to(): string
|
|
{
|
|
return $this->time(self::KEY_TO, self::DEFAULT_TO);
|
|
}
|
|
|
|
private function time(string $key, string $default): string
|
|
{
|
|
$value = (string) Settings::get($key, $default);
|
|
|
|
return preg_match('/^([01]\d|2[0-3]):[0-5]\d$/', $value) === 1 ? $value : $default;
|
|
}
|
|
|
|
/**
|
|
* Is now inside the window?
|
|
*
|
|
* On the wall clock, not UTC (R19): an operator who writes 05:00 means five
|
|
* in the morning where they live, and a window that drifts by an hour twice
|
|
* a year is a window nobody trusts.
|
|
*
|
|
* A window that ends before it starts wraps past midnight — 23:00 to 02:00
|
|
* is a perfectly reasonable thing to ask for, and reading it as an empty
|
|
* range would silently mean "never".
|
|
*/
|
|
public function isOpen(?Carbon $at = null): bool
|
|
{
|
|
$now = ($at ?? Carbon::now())->local();
|
|
|
|
if (! in_array((int) $now->isoWeekday(), $this->days(), true)) {
|
|
// …unless the window wraps and we are in the tail of yesterday's.
|
|
return $this->wraps()
|
|
&& in_array((int) $now->copy()->subDay()->isoWeekday(), $this->days(), true)
|
|
&& $now->format('H:i') < $this->to();
|
|
}
|
|
|
|
$clock = $now->format('H:i');
|
|
$from = $this->from();
|
|
$to = $this->to();
|
|
|
|
return $this->wraps()
|
|
? ($clock >= $from || $clock < $to)
|
|
: ($clock >= $from && $clock < $to);
|
|
}
|
|
|
|
private function wraps(): bool
|
|
{
|
|
return $this->to() <= $this->from();
|
|
}
|
|
}
|