Merge branch 'plugin-cringe' into 'master'

expose scheduled tasks to plugins, switch cache_starred_images plugin to use...

See merge request tt-rss/tt-rss!129
This commit is contained in:
Andrew Dolgov
2025-05-04 17:40:13 +00:00
8 changed files with 144 additions and 82 deletions

View File

@@ -6,7 +6,7 @@ class Config {
const T_STRING = 2;
const T_INT = 3;
const SCHEMA_VERSION = 150;
const SCHEMA_VERSION = 151;
/** override default values, defined below in _DEFAULTS[], prefixing with _ENVVAR_PREFIX:
*

View File

@@ -38,6 +38,8 @@ class PluginHost {
private static ?PluginHost $instance = null;
private ?Scheduler $scheduler = null;
const API_VERSION = 2;
const PUBLIC_METHOD_DELIMITER = "--";
@@ -215,6 +217,7 @@ class PluginHost {
function __construct() {
$this->pdo = Db::pdo();
$this->scheduler = new Scheduler('PluginHost Scheduler');
$this->storage = [];
}
@@ -438,6 +441,10 @@ class PluginHost {
$this->owner_uid = (int) $owner_uid;
if ($this->owner_uid) {
$this->set_scheduler_name("PluginHost Scheduler for UID $owner_uid");
}
foreach ($plugins as $class) {
$class = trim($class);
$class_file = strtolower(basename(clean($class)));
@@ -907,4 +914,30 @@ class PluginHost {
return basename(dirname(dirname($ref->getFileName()))) == "plugins.local";
}
/**
* This exposes sheduled tasks functionality to plugins. For system plugins, tasks registered here are
* executed (if due) during housekeeping. For user plugins, tasks are only run after any feeds owned by
* the user have been processed in an update batch (which means user is not inactive).
*
* This behaviour mirrors that of `HOOK_HOUSE_KEEPING` for user plugins.
*
* @see Scheduler::add_scheduled_task()
* @see Plugin::hook_house_keeping()
*/
function add_scheduled_task(Plugin $sender, string $task_name, string $cron_expression, Closure $callback): bool {
if ($this->is_system($sender))
$task_name = get_class($sender) . ':' . $task_name;
else
$task_name = get_class($sender) . ':' . $task_name . ':' . $this->owner_uid;
return $this->scheduler->add_scheduled_task($task_name, $cron_expression, $callback);
}
function run_due_tasks() : void {
$this->scheduler->run_due_tasks();
}
private function set_scheduler_name(string $name) : void {
$this->scheduler->set_name($name);
}
}

View File

@@ -33,6 +33,7 @@ class Pref_System extends Handler_Administrative {
<table width='100%' class='event-log'>
<tr>
<th><?= __("Task name") ?></th>
<th><?= __("Schedule") ?></th>
<th><?= __("Last executed") ?></th>
<th><?= __("Duration (seconds)") ?></th>
<th><?= __("Return code") ?></th>
@@ -40,6 +41,7 @@ class Pref_System extends Handler_Administrative {
<?php
$task_records = ORM::for_table('ttrss_scheduled_tasks')
->order_by_asc(['last_cron_expression', 'task_name'])
->find_many();
foreach ($task_records as $task) {
@@ -48,6 +50,7 @@ class Pref_System extends Handler_Administrative {
?>
<tr>
<td class="<?= $row_style ?>"><?= $task->task_name ?></td>
<td><?= $task->last_cron_expression ?></td>
<td><?= TimeHelper::make_local_datetime($task->last_run) ?></td>
<td><?= $task->last_duration ?></td>
<td><?= $task->last_rc ?></td>

View File

@@ -1666,6 +1666,7 @@ class RSSUtils {
UserHelper::load_user_plugins($owner_uid, $tmph);
$tmph->run_due_tasks();
$tmph->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
@@ -1794,7 +1795,10 @@ class RSSUtils {
static function housekeeping_common(): void {
Scheduler::getInstance()->run_due_tasks();
PluginHost::getInstance()->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
$pluginhost = PluginHost::getInstance();
$pluginhost->run_due_tasks();
$pluginhost->run_hooks(PluginHost::HOOK_HOUSE_KEEPING);
}
static function update_favicon(string $site_url, int $feed): false|string {

View File

@@ -3,16 +3,23 @@ class Scheduler {
private static ?Scheduler $instance = null;
const TASK_RC_EXCEPTION = -100;
const DEFAULT_NAME = 'Default Scheduler';
/** @var array<string, mixed> */
private array $scheduled_tasks = [];
function __construct() {
$this->add_scheduled_task('purge_orphaned_scheduled_tasks', '@weekly',
function() {
return $this->purge_orphaned_tasks();
}
);
private string $name;
function __construct(string $name = self::DEFAULT_NAME) {
$this->set_name($name);
if ($name === self::DEFAULT_NAME) {
$this->add_scheduled_task('purge_orphaned_scheduled_tasks', '@weekly',
function() {
return $this->purge_orphaned_tasks();
}
);
}
}
public static function getInstance(): Scheduler {
@@ -22,6 +29,11 @@ class Scheduler {
return self::$instance;
}
/** Sets specific identifier for this instance of Scheduler used in debug logging */
public function set_name(string $name) : void {
$this->name = $name;
}
/**
* Adds a backend scheduled task which will be executed by updater (if due) during housekeeping.
*
@@ -42,13 +54,13 @@ class Scheduler {
$task_name = strtolower($task_name);
if (isset($this->scheduled_tasks[$task_name])) {
user_error("Attempted to override already registered scheduled task $task_name", E_USER_WARNING);
user_error("[$this->name] Attempted to override already registered scheduled task $task_name", E_USER_WARNING);
return false;
} else {
try {
$cron = new Cron\CronExpression($cron_expression);
} catch (InvalidArgumentException $e) {
user_error("Attempt to register scheduled task $task_name failed: " . $e->getMessage(), E_USER_WARNING);
user_error("[$this->name] Attempt to register scheduled task $task_name failed: " . $e->getMessage(), E_USER_WARNING);
return false;
}
@@ -64,7 +76,7 @@ class Scheduler {
* Execute scheduled tasks which are due to run and record last run timestamps.
*/
function run_due_tasks() : void {
Debug::log('Processing all scheduled tasks...');
Debug::log("[$this->name] Processing all scheduled tasks...");
$tasks_succeeded = 0;
$tasks_failed = 0;
@@ -89,7 +101,7 @@ class Scheduler {
try {
$rc = (int) $task['callback']();
} catch (Exception $e) {
user_error("Scheduled task $task_name failed with exception: " . $e->getMessage(), E_USER_WARNING);
user_error("[$this->name] Scheduled task $task_name failed with exception: " . $e->getMessage(), E_USER_WARNING);
$rc = self::TASK_RC_EXCEPTION;
}
@@ -108,6 +120,7 @@ class Scheduler {
$task_record->last_run = Db::NOW();
$task_record->last_duration = $task_duration;
$task_record->last_rc = $rc;
$task_record->last_cron_expression = $task['cron']->getExpression();
$task_record->save();
} else {
@@ -118,6 +131,7 @@ class Scheduler {
'last_duration' => $task_duration,
'last_rc' => $rc,
'last_run' => Db::NOW(),
'last_cron_expression' => $task['cron']->getExpression()
]);
$task_record->save();
@@ -125,7 +139,7 @@ class Scheduler {
}
}
Debug::log("Processing scheduled tasks finished with $tasks_succeeded tasks succeeded and $tasks_failed tasks failed.");
Debug::log("[$this->name] Processing scheduled tasks finished with $tasks_succeeded tasks succeeded and $tasks_failed tasks failed.");
}
/**

View File

@@ -31,7 +31,74 @@ class Cache_Starred_Images extends Plugin {
$this->cache_status->put(".no-auto-expiry", "");
if ($this->cache->is_writable() && $this->cache_status->is_writable()) {
$host->add_hook($host::HOOK_HOUSE_KEEPING, $this);
$host->add_scheduled_task($this, "cache_starred_images", "@hourly", function() {
Debug::log("caching media of starred articles for user " . $this->host->get_owner_uid() . "...");
$sth = $this->pdo->prepare("SELECT content, ttrss_entries.title,
ttrss_user_entries.owner_uid, link, site_url, ttrss_entries.id, plugin_data
FROM ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON
(ttrss_user_entries.feed_id = ttrss_feeds.id)
WHERE ref_id = ttrss_entries.id AND
marked = true AND
site_url != '' AND
ttrss_user_entries.owner_uid = ? AND
plugin_data NOT LIKE '%starred_cache_images%'
ORDER BY RANDOM() LIMIT 100");
if ($sth->execute([$this->host->get_owner_uid()])) {
$usth = $this->pdo->prepare("UPDATE ttrss_entries SET plugin_data = ? WHERE id = ?");
while ($line = $sth->fetch()) {
Debug::log("processing article " . $line["title"], Debug::LOG_VERBOSE);
if ($line["site_url"]) {
$success = $this->cache_article_images($line["content"], $line["site_url"], $line["owner_uid"], $line["id"]);
if ($success) {
$plugin_data = "starred_cache_images," . $line["owner_uid"] . ":" . $line["plugin_data"];
$usth->execute([$plugin_data, $line['id']]);
}
}
}
}
});
$host->add_scheduled_task($this, "expire_caches", "@daily", function() {
Debug::log("expiring {$this->cache->get_dir()} and {$this->cache_status->get_dir()}...");
$files = [
...(glob($this->cache->get_dir() . "/*-*") ?: []),
...(glob($this->cache_status->get_dir() . "/*.status") ?: []),
];
asort($files);
$last_article_id = 0;
$article_exists = 1;
foreach ($files as $file) {
list ($article_id, $hash) = explode("-", basename($file));
if ($article_id != $last_article_id) {
$last_article_id = $article_id;
$sth = $this->pdo->prepare("SELECT id FROM ttrss_entries WHERE id = ?");
$sth->execute([$article_id]);
$article_exists = $sth->fetch();
}
if (!$article_exists) {
unlink($file);
}
}
});
$host->add_hook($host::HOOK_ENCLOSURE_ENTRY, $this);
$host->add_hook($host::HOOK_SANITIZE, $this);
} else {
@@ -39,73 +106,6 @@ class Cache_Starred_Images extends Plugin {
}
}
/** since HOOK_UPDATE_TASK is not available to user plugins, this hook is a next best thing */
function hook_house_keeping() {
Debug::log("caching media of starred articles for user " . $this->host->get_owner_uid() . "...");
$sth = $this->pdo->prepare("SELECT content, ttrss_entries.title,
ttrss_user_entries.owner_uid, link, site_url, ttrss_entries.id, plugin_data
FROM ttrss_entries, ttrss_user_entries LEFT JOIN ttrss_feeds ON
(ttrss_user_entries.feed_id = ttrss_feeds.id)
WHERE ref_id = ttrss_entries.id AND
marked = true AND
site_url != '' AND
ttrss_user_entries.owner_uid = ? AND
plugin_data NOT LIKE '%starred_cache_images%'
ORDER BY RANDOM() LIMIT 100");
if ($sth->execute([$this->host->get_owner_uid()])) {
$usth = $this->pdo->prepare("UPDATE ttrss_entries SET plugin_data = ? WHERE id = ?");
while ($line = $sth->fetch()) {
Debug::log("processing article " . $line["title"], Debug::LOG_VERBOSE);
if ($line["site_url"]) {
$success = $this->cache_article_images($line["content"], $line["site_url"], $line["owner_uid"], $line["id"]);
if ($success) {
$plugin_data = "starred_cache_images," . $line["owner_uid"] . ":" . $line["plugin_data"];
$usth->execute([$plugin_data, $line['id']]);
}
}
}
}
/* actual housekeeping */
Debug::log("expiring {$this->cache->get_dir()} and {$this->cache_status->get_dir()}...");
$files = [
...(glob($this->cache->get_dir() . "/*-*") ?: []),
...(glob($this->cache_status->get_dir() . "/*.status") ?: []),
];
asort($files);
$last_article_id = 0;
$article_exists = 1;
foreach ($files as $file) {
list ($article_id, $hash) = explode("-", basename($file));
if ($article_id != $last_article_id) {
$last_article_id = $article_id;
$sth = $this->pdo->prepare("SELECT id FROM ttrss_entries WHERE id = ?");
$sth->execute([$article_id]);
$article_exists = $sth->fetch();
}
if (!$article_exists) {
unlink($file);
}
}
}
function hook_enclosure_entry($enc, $article_id, $rv) {
$local_filename = $article_id . "-" . sha1($enc["content_url"]);

View File

@@ -0,0 +1,6 @@
alter table ttrss_scheduled_tasks add column owner_uid integer default null references ttrss_users(id) ON DELETE CASCADE;
alter table ttrss_scheduled_tasks add column last_cron_expression varchar(250);
update ttrss_scheduled_tasks set last_cron_expression = '';
alter table ttrss_scheduled_tasks alter column last_cron_expression set not null;

View File

@@ -400,6 +400,8 @@ create table ttrss_scheduled_tasks(
task_name varchar(250) unique not null,
last_duration integer not null,
last_rc integer not null,
last_run timestamp not null default NOW());
last_run timestamp not null default NOW(),
last_cron_expression varchar(250) not null,
owner_uid integer default null references ttrss_users(id) ON DELETE CASCADE);
commit;