diff --git a/inc/functions.php b/inc/functions.php index 50e0ca38..71231203 100755 --- a/inc/functions.php +++ b/inc/functions.php @@ -428,10 +428,10 @@ function rebuildThemes($action, $boardname = false) { $board = $_board; // Reload the locale - if ($config['locale'] != $current_locale) { - $current_locale = $config['locale']; - init_locale($config['locale']); - } + if ($config['locale'] != $current_locale) { + $current_locale = $config['locale']; + init_locale($config['locale']); + } if (PHP_SAPI === 'cli') { echo "Rebuilding theme ".$theme['theme']."... "; @@ -450,8 +450,8 @@ function rebuildThemes($action, $boardname = false) { // Reload the locale if ($config['locale'] != $current_locale) { - $current_locale = $config['locale']; - init_locale($config['locale']); + $current_locale = $config['locale']; + init_locale($config['locale']); } } @@ -2918,7 +2918,20 @@ function generation_strategy($fun, $array=array()) { global $config; return 'rebuild'; case 'defer': // Ok, it gets interesting here :) - get_queue('generate')->push(serialize(array('build', $fun, $array, $action))); + $queue = Queues::get_queue($config, 'generate'); + if ($queue === false) { + if ($config['syslog']) { + _syslog(LOG_ERR, "Could not initialize generate queue, falling back to immediate rebuild strategy"); + } + return 'rebuild'; + } + $ret = $queue->push(serialize(array('build', $fun, $array, $action))); + if ($ret === false) { + if ($config['syslog']) { + _syslog(LOG_ERR, "Could not push item in the queue, falling back to immediate rebuild strategy"); + } + return 'rebuild'; + } return 'ignore'; case 'build_on_load': return 'delete'; diff --git a/inc/lock.php b/inc/lock.php index 4fb2f5df..5a83562a 100644 --- a/inc/lock.php +++ b/inc/lock.php @@ -1,39 +1,84 @@ f = fopen("tmp/locks/$key", "w"); - } - } +class Locks { + private static function filesystem(string $key): Lock|false { + $key = str_replace('/', '::', $key); + $key = str_replace("\0", '', $key); - // Get a shared lock - function get($nonblock = false) { global $config; - if ($config['lock']['enabled'] == 'fs') { - $wouldblock = false; - flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock); - if ($nonblock && $wouldblock) return false; - } - return $this; - } + $fd = fopen("tmp/locks/$key", "w"); + if ($fd === false) { + return false; + } - // Get an exclusive lock - function get_ex($nonblock = false) { global $config; - if ($config['lock']['enabled'] == 'fs') { - $wouldblock = false; - flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock); - if ($nonblock && $wouldblock) return false; - } - return $this; - } + return new class($fd) implements Lock { + // Resources have no type in php. + private mixed $f; - // Free a lock - function free() { global $config; - if ($config['lock']['enabled'] == 'fs') { - flock($this->f, LOCK_UN); - } - return $this; - } + + function __construct($fd) { + $this->f = $fd; + } + + public function get(bool $nonblock = false): Lock|false { + $wouldblock = false; + flock($this->f, LOCK_SH | ($nonblock ? LOCK_NB : 0), $wouldblock); + if ($nonblock && $wouldblock) { + return false; + } + return $this; + } + + public function get_ex(bool $nonblock = false): Lock|false { + $wouldblock = false; + flock($this->f, LOCK_EX | ($nonblock ? LOCK_NB : 0), $wouldblock); + if ($nonblock && $wouldblock) { + return false; + } + return $this; + } + + public function free(): Lock { + flock($this->f, LOCK_UN); + return $this; + } + }; + } + + /** + * No-op. Can be used for mocking. + */ + public static function none(): Lock|false { + return new class() implements Lock { + public function get(bool $nonblock = false): Lock|false { + return $this; + } + + public function get_ex(bool $nonblock = false): Lock|false { + return $this; + } + + public function free(): Lock { + return $this; + } + }; + } + + public static function get_lock(array $config, string $key): Lock|false { + if ($config['lock']['enabled'] == 'fs') { + return self::filesystem($key); + } else { + return self::none(); + } + } +} + +interface Lock { + // Get a shared lock + public function get(bool $nonblock = false): Lock|false; + + // Get an exclusive lock + public function get_ex(bool $nonblock = false): Lock|false; + + // Free a lock + public function free(): Lock; } diff --git a/inc/queue.php b/inc/queue.php index 66305b3b..a5905c84 100644 --- a/inc/queue.php +++ b/inc/queue.php @@ -1,49 +1,98 @@ lock = new Lock($key); - $key = str_replace('/', '::', $key); - $key = str_replace("\0", '', $key); - $this->key = "tmp/queue/$key/"; - } - } +class Queues { + private static $queues = array(); - function push($str) { global $config; - if ($config['queue']['enabled'] == 'fs') { - $this->lock->get_ex(); - file_put_contents($this->key.microtime(true), $str); - $this->lock->free(); - } - return $this; - } - function pop($n = 1) { global $config; - if ($config['queue']['enabled'] == 'fs') { - $this->lock->get_ex(); - $dir = opendir($this->key); - $paths = array(); - while ($n > 0) { - $path = readdir($dir); - if ($path === FALSE) break; - elseif ($path == '.' || $path == '..') continue; - else { $paths[] = $path; $n--; } - } - $out = array(); - foreach ($paths as $v) { - $out []= file_get_contents($this->key.$v); - unlink($this->key.$v); - } - $this->lock->free(); - return $out; - } - } + /** + * This queue implementation isn't actually ordered, so it works more as a "bag". + */ + private static function filesystem(string $key, Lock $lock): Queue { + $key = str_replace('/', '::', $key); + $key = str_replace("\0", '', $key); + $key = "tmp/queue/$key/"; + + return new class($key, $lock) implements Queue { + private Lock $lock; + private string $key; + + + function __construct(string $key, Lock $lock) { + $this->lock = $lock; + $this->key = $key; + } + + public function push(string $str): bool { + $this->lock->get_ex(); + $ret = file_put_contents($this->key . microtime(true), $str); + $this->lock->free(); + return $ret !== false; + } + + public function pop(int $n = 1): array { + $this->lock->get_ex(); + $dir = opendir($this->key); + $paths = array(); + + while ($n > 0) { + $path = readdir($dir); + if ($path === false) { + break; + } elseif ($path == '.' || $path == '..') { + continue; + } else { + $paths[] = $path; + $n--; + } + } + + $out = array(); + foreach ($paths as $v) { + $out[] = file_get_contents($this->key . $v); + unlink($this->key . $v); + } + + $this->lock->free(); + return $out; + } + }; + } + + /** + * No-op. Can be used for mocking. + */ + public static function none(): Queue { + return new class() implements Queue { + public function push(string $str): bool { + return true; + } + + public function pop(int $n = 1): array { + return array(); + } + }; + } + + public static function get_queue(array $config, string $name): Queue|false { + if (!isset(self::$queues[$name])) { + if ($config['queue']['enabled'] == 'fs') { + $lock = Locks::get_lock($config, $name); + if ($lock === false) { + return false; + } + self::$queues[$name] = self::filesystem($name, $lock); + } else { + self::$queues[$name] = self::none(); + } + } + return self::$queues[$name]; + } } -// Don't use the constructor. Use the get_queue function. -$queues = array(); +interface Queue { + // Push a string in the queue. + public function push(string $str): bool; -function get_queue($name) { global $queues; - return $queues[$name] = isset ($queues[$name]) ? $queues[$name] : new Queue($name); + // Get a string from the queue. + public function pop(int $n = 1): array; }