Warn about sync tasks not having been run recently.

Also includes a minor rewrite of the EventDispatcher, and a restructure of notification checks into standalone classes.
This commit is contained in:
Buster "Silver Eagle" Neece 2020-11-12 15:30:02 -06:00
parent 5cab595bcf
commit 582b8faef9
No known key found for this signature in database
GPG Key ID: 6D9E12FF03411F4E
11 changed files with 391 additions and 284 deletions

View File

@ -6,60 +6,61 @@ use App\Middleware;
use App\Settings;
return function (App\EventDispatcher $dispatcher) {
$dispatcher->addListener(Event\BuildConsoleCommands::class, function (Event\BuildConsoleCommands $event) use ($dispatcher) {
$console = $event->getConsole();
$di = $console->getContainer();
$dispatcher->addListener(Event\BuildConsoleCommands::class,
function (Event\BuildConsoleCommands $event) use ($dispatcher) {
$console = $event->getConsole();
$di = $console->getContainer();
/** @var Settings $settings */
$settings = $di->get(Settings::class);
/** @var Settings $settings */
$settings = $di->get(Settings::class);
if ($settings->enableRedis()) {
$console->command('cache:clear', Command\ClearCacheCommand::class)
->setDescription('Clear all application caches.');
}
if ($settings->enableRedis()) {
$console->command('cache:clear', Command\ClearCacheCommand::class)
->setDescription('Clear all application caches.');
}
if ($settings->enableDatabase()) {
// Doctrine ORM/DBAL
Doctrine\ORM\Tools\Console\ConsoleRunner::addCommands($console);
if ($settings->enableDatabase()) {
// Doctrine ORM/DBAL
Doctrine\ORM\Tools\Console\ConsoleRunner::addCommands($console);
// Add Doctrine Migrations
/** @var Doctrine\ORM\EntityManagerInterface $em */
$em = $di->get(Doctrine\ORM\EntityManagerInterface::class);
// Add Doctrine Migrations
/** @var Doctrine\ORM\EntityManagerInterface $em */
$em = $di->get(Doctrine\ORM\EntityManagerInterface::class);
$helper_set = $console->getHelperSet();
$doctrine_helpers = Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet($em);
$helper_set->set($doctrine_helpers->get('db'), 'db');
$helper_set->set($doctrine_helpers->get('em'), 'em');
$helper_set = $console->getHelperSet();
$doctrine_helpers = Doctrine\ORM\Tools\Console\ConsoleRunner::createHelperSet($em);
$helper_set->set($doctrine_helpers->get('db'), 'db');
$helper_set->set($doctrine_helpers->get('em'), 'em');
$migrationConfigurations = [
'migrations_paths' => [
'App\Entity\Migration' => $settings[Settings::BASE_DIR] . '/src/Entity/Migration',
],
'table_storage' => [
'table_name' => 'app_migrations',
'version_column_length' => 191,
],
];
$migrationConfigurations = [
'migrations_paths' => [
'App\Entity\Migration' => $settings[Settings::BASE_DIR] . '/src/Entity/Migration',
],
'table_storage' => [
'table_name' => 'app_migrations',
'version_column_length' => 191,
],
];
$buildMigrationConfigurationsEvent = new Event\BuildMigrationConfigurationArray(
$migrationConfigurations,
$settings[Settings::BASE_DIR]
);
$dispatcher->dispatch($buildMigrationConfigurationsEvent);
$buildMigrationConfigurationsEvent = new Event\BuildMigrationConfigurationArray(
$migrationConfigurations,
$settings[Settings::BASE_DIR]
);
$dispatcher->dispatch($buildMigrationConfigurationsEvent);
$migrationConfigurations = $buildMigrationConfigurationsEvent->getMigrationConfigurations();
$migrationConfigurations = $buildMigrationConfigurationsEvent->getMigrationConfigurations();
$migrateConfig = new Doctrine\Migrations\Configuration\Migration\ConfigurationArray($migrationConfigurations);
$migrateConfig = new Doctrine\Migrations\Configuration\Migration\ConfigurationArray($migrationConfigurations);
$migrateFactory = Doctrine\Migrations\DependencyFactory::fromEntityManager(
$migrateConfig,
new Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager($em)
);
Doctrine\Migrations\Tools\Console\ConsoleRunner::addCommands($console, $migrateFactory);
}
$migrateFactory = Doctrine\Migrations\DependencyFactory::fromEntityManager(
$migrateConfig,
new Doctrine\Migrations\Configuration\EntityManager\ExistingEntityManager($em)
);
Doctrine\Migrations\Tools\Console\ConsoleRunner::addCommands($console, $migrateFactory);
}
call_user_func(include(__DIR__ . '/cli.php'), $console);
});
call_user_func(include(__DIR__ . '/cli.php'), $console);
});
$dispatcher->addListener(Event\BuildRoutes::class, function (Event\BuildRoutes $event) {
$app = $event->getApp();
@ -115,15 +116,35 @@ return function (App\EventDispatcher $dispatcher) {
});
// Other event subscribers from across the application.
$dispatcher->addCallableListener(
Event\GetSyncTasks::class,
App\Sync\TaskLocator::class
);
$dispatcher->addCallableListener(
Event\GetNotifications::class,
App\Notification\Check\ComposeVersionCheck::class
);
$dispatcher->addCallableListener(
Event\GetNotifications::class,
App\Notification\Check\UpdateCheck::class
);
$dispatcher->addCallableListener(
Event\GetNotifications::class,
App\Notification\Check\RecentBackupCheck::class
);
$dispatcher->addCallableListener(
Event\GetNotifications::class,
App\Notification\Check\SyncTaskCheck::class
);
$dispatcher->addServiceSubscriber([
App\Radio\AutoDJ\Queue::class,
App\Radio\AutoDJ\Annotations::class,
App\Radio\Backend\Liquidsoap\ConfigWriter::class,
App\Sync\Task\NowPlaying::class,
App\Sync\TaskLocator::class,
App\Webhook\Dispatcher::class,
App\Controller\Api\NowplayingController::class,
App\Notification\Manager::class,
]);
};

View File

@ -70,7 +70,7 @@ class DashboardController
}
// Get administrator notifications.
$notification_event = new Event\GetNotifications($user, $request);
$notification_event = new Event\GetNotifications($request);
$this->dispatcher->dispatch($notification_event);
$notifications = $notification_event->getNotifications();

View File

@ -2,32 +2,22 @@
namespace App\Event;
use App\Entity\User;
use App\Http\ServerRequest;
use App\Notification\Notification;
use Symfony\Contracts\EventDispatcher\Event;
class GetNotifications extends Event
{
protected User $current_user;
protected ServerRequest $request;
protected array $notifications;
public function __construct(User $current_user, ServerRequest $request)
public function __construct(ServerRequest $request)
{
$this->current_user = $current_user;
$this->request = $request;
$this->notifications = [];
}
public function getCurrentUser(): User
{
return $this->current_user;
}
public function getRequest(): ServerRequest
{
return $this->request;

View File

@ -18,29 +18,38 @@ class EventDispatcher extends \Symfony\Component\EventDispatcher\EventDispatcher
$this->callableResolver = $callableResolver;
}
public function addServiceSubscriber($class_name): void
/**
* @param class-string|class-string[] $className
*/
public function addServiceSubscriber($className): void
{
if (is_array($class_name)) {
foreach ($class_name as $service) {
if (is_array($className)) {
foreach ($className as $service) {
$this->addServiceSubscriber($service);
}
return;
}
foreach ($class_name::getSubscribedEvents() as $eventName => $params) {
foreach ($className::getSubscribedEvents() as $eventName => $params) {
if (is_string($params)) {
$this->addListener($eventName, $this->getCallable($class_name, $params));
} elseif (is_string($params[0])) {
$this->addListener(
$this->addCallableListener(
$eventName,
$this->getCallable($class_name, $params[0]),
$className,
$params
);
} elseif (is_string($params[0])) {
$this->addCallableListener(
$eventName,
$className,
$params[0],
$params[1] ?? 0
);
} else {
foreach ($params as $listener) {
$this->addListener(
$this->addCallableListener(
$eventName,
$this->getCallable($class_name, $listener[0]),
$className,
$listener[0],
$listener[1] ?? 0
);
}
@ -48,31 +57,65 @@ class EventDispatcher extends \Symfony\Component\EventDispatcher\EventDispatcher
}
}
public function removeServiceSubscriber($class_name): void
/**
* @param class-string|class-string[] $className
*/
public function removeServiceSubscriber($className): void
{
if (is_array($class_name)) {
foreach ($class_name as $service) {
if (is_array($className)) {
foreach ($className as $service) {
$this->removeServiceSubscriber($service);
}
return;
}
foreach ($class_name::getSubscribedEvents() as $eventName => $params) {
foreach ($className::getSubscribedEvents() as $eventName => $params) {
if (is_array($params) && is_array($params[0])) {
foreach ($params as $listener) {
$this->removeListener($eventName, $this->getCallable($class_name, $listener[0]));
$this->removeCallableListener(
$eventName,
$className,
$listener[0]
);
}
} else {
$this->removeListener(
$this->removeCallableListener(
$eventName,
$this->getCallable($class_name, is_string($params) ? $params : $params[0])
$className,
is_string($params) ? $params : $params[0]
);
}
}
}
protected function getCallable($class_name, $method): DeferredCallable
{
return new DeferredCallable($class_name . ':' . $method, $this->callableResolver);
public function addCallableListener(
string $eventName,
string $className,
?string $method = '__invoke',
int $priority = 0
): void {
$this->addListener(
$eventName,
$this->getCallable($className, $method),
$priority
);
}
public function removeCallableListener(
string $eventName,
string $className,
?string $method = '__invoke'
): void {
$this->removeListener(
$eventName,
$this->getCallable($className, $method)
);
}
protected function getCallable(
string $className,
?string $method = '__invoke'
): DeferredCallable {
return new DeferredCallable($className . ':' . $method, $this->callableResolver);
}
}

View File

@ -0,0 +1,47 @@
<?php
namespace App\Notification\Check;
use App\Acl;
use App\Event\GetNotifications;
use App\Notification\Notification;
use App\Settings;
class ComposeVersionCheck
{
protected Settings $appSettings;
public function __construct(Settings $appSettings)
{
$this->appSettings = $appSettings;
}
public function __invoke(GetNotifications $event): void
{
// This notification is for full administrators only.
$request = $event->getRequest();
$acl = $request->getAcl();
if (!$acl->userAllowed($request->getUser(), Acl::GLOBAL_ALL)) {
return;
}
if (!$this->appSettings->isDocker()) {
return;
}
$compose_revision = $_ENV['AZURACAST_DC_REVISION'] ?? 1;
if ($compose_revision < 5) {
$event->addNotification(new Notification(
__('Your <code>docker-compose.yml</code> file is out of date!'),
// phpcs:disable Generic.Files.LineLength
__(
'You should update your <code>docker-compose.yml</code> file to reflect the newest changes. View the <a href="%s" target="_blank">latest version of the file</a> and update your file accordingly.<br>You can also use the <code>./docker.sh</code> utility script to automatically update your file.',
'https://raw.githubusercontent.com/AzuraCast/AzuraCast/master/docker-compose.sample.yml'
),
// phpcs:enable
Notification::WARNING
));
}
}
}

View File

@ -0,0 +1,56 @@
<?php
namespace App\Notification\Check;
use App\Acl;
use App\Entity;
use App\Event\GetNotifications;
use App\Notification\Notification;
use App\Settings;
use Carbon\CarbonImmutable;
class RecentBackupCheck
{
protected Entity\Repository\SettingsRepository $settingsRepo;
protected Settings $appSettings;
public function __construct(Entity\Repository\SettingsRepository $settingsRepo, Settings $appSettings)
{
$this->settingsRepo = $settingsRepo;
$this->appSettings = $appSettings;
}
public function __invoke(GetNotifications $event): void
{
// This notification is for backup administrators only.
$request = $event->getRequest();
$acl = $request->getAcl();
if (!$acl->userAllowed($request->getUser(), Acl::GLOBAL_BACKUPS)) {
return;
}
if (!$this->appSettings->isProduction()) {
return;
}
$threshold = CarbonImmutable::now()->subWeeks(2)->getTimestamp();
$backupLastRun = $this->settingsRepo->getSetting(Entity\Settings::BACKUP_LAST_RUN, 0);
if ($backupLastRun < $threshold) {
$router = $request->getRouter();
$backupUrl = $router->named('admin:backups:index');
$event->addNotification(new Notification(
__('Installation Not Recently Backed Up'),
// phpcs:disable Generic.Files.LineLength
__(
'This installation has not been backed up in the last two weeks. Visit the <a href="%s" target="_blank">Backups</a> page to run a new backup.',
$backupUrl
),
// phpcs:enable
Notification::INFO
));
}
}
}

View File

@ -0,0 +1,52 @@
<?php
namespace App\Notification\Check;
use App\Acl;
use App\Event\GetNotifications;
use App\Notification\Notification;
use App\Sync\Runner;
class SyncTaskCheck
{
protected Runner $syncRunner;
public function __construct(Runner $syncRunner)
{
$this->syncRunner = $syncRunner;
}
public function __invoke(GetNotifications $event): void
{
// This notification is for full administrators only.
$request = $event->getRequest();
$acl = $request->getAcl();
if (!$acl->userAllowed($request->getUser(), Acl::GLOBAL_ALL)) {
return;
}
$syncTasks = $this->syncRunner->getSyncTimes();
foreach ($syncTasks as $taskKey => $task) {
$interval = $task['interval'];
$diff = $task['diff'];
if ($diff > ($interval * 5)) {
$router = $request->getRouter();
$backupUrl = $router->named('admin:debug:sync', ['type' => $taskKey]);
$event->addNotification(new Notification(
__('Synchronized Task Not Recently Run'),
// phpcs:disable Generic.Files.LineLength
__(
'The "%s" synchronization task has not run recently. This may indicate an error with your installation. <a href="%s" target="_blank">Manually run the task</a> to check for errors.',
$task['name'],
$backupUrl
),
// phpcs:enable
Notification::ERROR
));
}
}
}
}

View File

@ -0,0 +1,98 @@
<?php
namespace App\Notification\Check;
use App\Acl;
use App\Entity;
use App\Event\GetNotifications;
use App\Notification\Notification;
use App\Version;
class UpdateCheck
{
protected Entity\Repository\SettingsRepository $settingsRepo;
protected Version $version;
public function __construct(Entity\Repository\SettingsRepository $settingsRepo, Version $version)
{
$this->settingsRepo = $settingsRepo;
$this->version = $version;
}
public function __invoke(GetNotifications $event): void
{
// This notification is for full administrators only.
$request = $event->getRequest();
$acl = $request->getAcl();
if (!$acl->userAllowed($request->getUser(), Acl::GLOBAL_ALL)) {
return;
}
$checkForUpdates = (bool)$this->settingsRepo->getSetting(Entity\Settings::CENTRAL_UPDATES, 1);
if (!$checkForUpdates) {
return;
}
$updateData = $this->settingsRepo->getSetting(Entity\Settings::UPDATE_RESULTS);
if (empty($updateData)) {
return;
}
$instructions_url = 'https://www.azuracast.com/administration/system/updating.html';
$instructions_string = __(
'Follow the <a href="%s" target="_blank">update instructions</a> to update your installation.',
$instructions_url
);
$releaseChannel = $this->version->getReleaseChannel();
if (Version::RELEASE_CHANNEL_STABLE === $releaseChannel && $updateData['needs_release_update']) {
$notification_parts = [
'<b>' . __(
'AzuraCast <a href="%s" target="_blank">version %s</a> is now available.',
'https://github.com/AzuraCast/AzuraCast/releases',
$updateData['latest_release']
) . '</b>',
__(
'You are currently running version %s. Updating is highly recommended.',
$updateData['current_release']
),
$instructions_string,
];
$event->addNotification(new Notification(
__('New AzuraCast Release Version Available'),
implode(' ', $notification_parts),
Notification::INFO
));
return;
}
if (Version::RELEASE_CHANNEL_ROLLING === $releaseChannel && $updateData['needs_rolling_update']) {
$notification_parts = [];
if ($updateData['rolling_updates_available'] < 15 && !empty($updateData['rolling_updates_list'])) {
$notification_parts[] = __('The following improvements have been made since your last update:');
$notification_parts[] = nl2br('<ul><li>' . implode(
'</li><li>',
$updateData['rolling_updates_list']
) . '</li></ul>');
} else {
$notification_parts[] = '<b>' . __(
'Your installation is currently %d update(s) behind the latest version.',
$updateData['rolling_updates_available']
) . '</b>';
$notification_parts[] = __('You should update to take advantage of bug and security fixes.');
}
$notification_parts[] = $instructions_string;
$event->addNotification(new Notification(
__('New AzuraCast Updates Available'),
implode(' ', $notification_parts),
Notification::INFO
));
return;
}
}
}

View File

@ -1,190 +0,0 @@
<?php
namespace App\Notification;
use App\Acl;
use App\Entity;
use App\Event\GetNotifications;
use App\Settings;
use App\Version;
use Carbon\CarbonImmutable;
use Doctrine\ORM\EntityManagerInterface;
use Monolog\Logger;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
class Manager implements EventSubscriberInterface
{
protected Acl $acl;
protected EntityManagerInterface $em;
protected Logger $logger;
protected Entity\Repository\SettingsRepository $settingsRepo;
protected Settings $appSettings;
protected Version $version;
public function __construct(
Acl $acl,
EntityManagerInterface $em,
Entity\Repository\SettingsRepository $settingsRepo,
Logger $logger,
Settings $appSettings,
Version $version
) {
$this->acl = $acl;
$this->em = $em;
$this->logger = $logger;
$this->appSettings = $appSettings;
$this->settingsRepo = $settingsRepo;
$this->version = $version;
}
/**
* @return mixed[]
*/
public static function getSubscribedEvents(): array
{
return [
GetNotifications::class => [
['checkComposeVersion', 1],
['checkUpdates', 0],
['checkRecentBackup', -1],
],
];
}
public function checkComposeVersion(GetNotifications $event): void
{
// This notification is for full administrators only.
if (!$this->acl->userAllowed($event->getCurrentUser(), Acl::GLOBAL_ALL)) {
return;
}
if (!$this->appSettings->isDocker()) {
return;
}
$compose_revision = $_ENV['AZURACAST_DC_REVISION'] ?? 1;
if ($compose_revision < 5) {
$event->addNotification(new Notification(
__('Your <code>docker-compose.yml</code> file is out of date!'),
// phpcs:disable Generic.Files.LineLength
__(
'You should update your <code>docker-compose.yml</code> file to reflect the newest changes. View the <a href="%s" target="_blank">latest version of the file</a> and update your file accordingly.<br>You can also use the <code>./docker.sh</code> utility script to automatically update your file.',
'https://raw.githubusercontent.com/AzuraCast/AzuraCast/master/docker-compose.sample.yml'
),
// phpcs:enable
Notification::WARNING
));
}
}
public function checkUpdates(GetNotifications $event): void
{
// This notification is for full administrators only.
if (!$this->acl->userAllowed($event->getCurrentUser(), Acl::GLOBAL_ALL)) {
return;
}
$checkForUpdates = (bool)$this->settingsRepo->getSetting(Entity\Settings::CENTRAL_UPDATES, 1);
if (!$checkForUpdates) {
return;
}
$updateData = $this->settingsRepo->getSetting(Entity\Settings::UPDATE_RESULTS);
if (empty($updateData)) {
return;
}
$instructions_url = 'https://www.azuracast.com/administration/system/updating.html';
$instructions_string = __(
'Follow the <a href="%s" target="_blank">update instructions</a> to update your installation.',
$instructions_url
);
$releaseChannel = $this->version->getReleaseChannel();
if (Version::RELEASE_CHANNEL_STABLE === $releaseChannel && $updateData['needs_release_update']) {
$notification_parts = [
'<b>' . __(
'AzuraCast <a href="%s" target="_blank">version %s</a> is now available.',
'https://github.com/AzuraCast/AzuraCast/releases',
$updateData['latest_release']
) . '</b>',
__(
'You are currently running version %s. Updating is highly recommended.',
$updateData['current_release']
),
$instructions_string,
];
$event->addNotification(new Notification(
__('New AzuraCast Release Version Available'),
implode(' ', $notification_parts),
Notification::INFO
));
return;
}
if (Version::RELEASE_CHANNEL_ROLLING === $releaseChannel && $updateData['needs_rolling_update']) {
$notification_parts = [];
if ($updateData['rolling_updates_available'] < 15 && !empty($updateData['rolling_updates_list'])) {
$notification_parts[] = __('The following improvements have been made since your last update:');
$notification_parts[] = nl2br('<ul><li>' . implode(
'</li><li>',
$updateData['rolling_updates_list']
) . '</li></ul>');
} else {
$notification_parts[] = '<b>' . __(
'Your installation is currently %d update(s) behind the latest version.',
$updateData['rolling_updates_available']
) . '</b>';
$notification_parts[] = __('You should update to take advantage of bug and security fixes.');
}
$notification_parts[] = $instructions_string;
$event->addNotification(new Notification(
__('New AzuraCast Updates Available'),
implode(' ', $notification_parts),
Notification::INFO
));
return;
}
}
public function checkRecentBackup(GetNotifications $event): void
{
if (!$this->acl->userAllowed($event->getCurrentUser(), Acl::GLOBAL_BACKUPS)) {
return;
}
if (!$this->appSettings->isProduction()) {
return;
}
$threshold = CarbonImmutable::now()->subWeeks(2)->getTimestamp();
$backupLastRun = $this->settingsRepo->getSetting(Entity\Settings::BACKUP_LAST_RUN, 0);
if ($backupLastRun < $threshold) {
$router = $event->getRequest()->getRouter();
$backupUrl = $router->named('admin:backups:index');
$event->addNotification(new Notification(
__('Installation Not Recently Backed Up'),
// phpcs:disable Generic.Files.LineLength
__(
'This installation has not been backed up in the last two weeks. Visit the <a href="%s" target="_blank">Backups</a> page to run a new backup.',
$backupUrl
),
// phpcs:enable
Notification::INFO
));
}
}
}

View File

@ -149,6 +149,7 @@ class Runner
],
'lastRunSetting' => Entity\Settings::NOWPLAYING_LAST_RUN,
'timeout' => 600,
'interval' => 15,
],
GetSyncTasks::SYNC_SHORT => [
'name' => __('1-Minute Sync'),
@ -157,6 +158,7 @@ class Runner
],
'lastRunSetting' => Entity\Settings::SHORT_SYNC_LAST_RUN,
'timeout' => 600,
'interval' => 60,
],
GetSyncTasks::SYNC_MEDIUM => [
'name' => __('5-Minute Sync'),
@ -165,6 +167,7 @@ class Runner
],
'lastRunSetting' => Entity\Settings::MEDIUM_SYNC_LAST_RUN,
'timeout' => 600,
'interval' => 300,
],
GetSyncTasks::SYNC_LONG => [
'name' => __('1-Hour Sync'),
@ -174,6 +177,7 @@ class Runner
],
'lastRunSetting' => Entity\Settings::LONG_SYNC_LAST_RUN,
'timeout' => 1800,
'interval' => 3600,
],
];

View File

@ -4,10 +4,8 @@ namespace App\Sync;
use App\Event\GetSyncTasks;
use Psr\Container\ContainerInterface;
use Symfony\Component\EventDispatcher\EventSubscriberInterface;
use Symfony\Contracts\EventDispatcher\Event;
class TaskLocator implements EventSubscriberInterface
class TaskLocator
{
protected ContainerInterface $di;
@ -19,19 +17,7 @@ class TaskLocator implements EventSubscriberInterface
$this->tasks = $tasks;
}
/**
* @return mixed[]
*/
public static function getSubscribedEvents(): array
{
return [
GetSyncTasks::class => [
['assignTasks', 0],
],
];
}
public function assignTasks(GetSyncTasks $event): void
public function __invoke(GetSyncTasks $event): void
{
$type = $event->getType();
if (!isset($this->tasks[$type])) {