mirror of
https://code.mensbeam.com/MensBeam/Arsse.git
synced 2024-12-22 13:12:41 +00:00
Experimental forking service and accompanying CLI
- Improves #48, #57, and #61
This commit is contained in:
parent
70f76f77fa
commit
1b970cc7c5
11 changed files with 249 additions and 61 deletions
15
arsse.php
15
arsse.php
|
@ -1,10 +1,19 @@
|
|||
<?php
|
||||
namespace JKingWeb\Arsse;
|
||||
var_export(get_defined_constants());
|
||||
exit;
|
||||
require_once __DIR__.DIRECTORY_SEPARATOR."bootstrap.php";
|
||||
Arsse::load(new Conf());
|
||||
|
||||
if(\PHP_SAPI=="cli") {
|
||||
(new Service)->watch();
|
||||
// initialize the CLI; this automatically handles --help and --version
|
||||
$cli = new CLI;
|
||||
// load configuration
|
||||
Arsse::load(new Conf());
|
||||
// handle CLI requests
|
||||
$cli->dispatch();
|
||||
} else {
|
||||
(new REST)->dispatch();
|
||||
// load configuration
|
||||
Arsse::load(new Conf());
|
||||
// handle Web requests
|
||||
(new REST)->dispatch()->output();
|
||||
}
|
|
@ -25,7 +25,8 @@
|
|||
"fguillot/picofeed": ">=0.1.31",
|
||||
"jkingweb/druuid": "^3.0.0",
|
||||
"phpseclib/phpseclib": "^2.0",
|
||||
"hosteurope/password-generator": "^1.0"
|
||||
"hosteurope/password-generator": "^1.0",
|
||||
"docopt/docopt": "^1.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"mikey179/vfsStream": "^1.6",
|
||||
|
|
62
composer.lock
generated
62
composer.lock
generated
|
@ -4,8 +4,54 @@
|
|||
"Read more about it at https://getcomposer.org/doc/01-basic-usage.md#composer-lock-the-lock-file",
|
||||
"This file is @generated automatically"
|
||||
],
|
||||
"content-hash": "f86e3cf99b80693dffb2a1e47e0b657d",
|
||||
"content-hash": "360a767ae23dbd1b702c1b3b8b08b683",
|
||||
"packages": [
|
||||
{
|
||||
"name": "docopt/docopt",
|
||||
"version": "1.0.2",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/docopt/docopt.php.git",
|
||||
"reference": "d2ee65c2fe4be78f945a48edd02be45843b39423"
|
||||
},
|
||||
"dist": {
|
||||
"type": "zip",
|
||||
"url": "https://api.github.com/repos/docopt/docopt.php/zipball/d2ee65c2fe4be78f945a48edd02be45843b39423",
|
||||
"reference": "d2ee65c2fe4be78f945a48edd02be45843b39423",
|
||||
"shasum": ""
|
||||
},
|
||||
"require": {
|
||||
"php": ">=5.3.0"
|
||||
},
|
||||
"require-dev": {
|
||||
"phpunit/phpunit": "4.1.*"
|
||||
},
|
||||
"type": "library",
|
||||
"autoload": {
|
||||
"classmap": [
|
||||
"src/docopt.php"
|
||||
]
|
||||
},
|
||||
"notification-url": "https://packagist.org/downloads/",
|
||||
"license": [
|
||||
"MIT"
|
||||
],
|
||||
"authors": [
|
||||
{
|
||||
"name": "Blake Williams",
|
||||
"email": "code@shabbyrobe.org",
|
||||
"homepage": "http://docopt.org/",
|
||||
"role": "Developer"
|
||||
}
|
||||
],
|
||||
"description": "Port of Python's docopt for PHP 5.3",
|
||||
"homepage": "http://github.com/docopt/docopt.php",
|
||||
"keywords": [
|
||||
"cli",
|
||||
"docs"
|
||||
],
|
||||
"time": "2015-10-30T03:21:23+00:00"
|
||||
},
|
||||
{
|
||||
"name": "fguillot/picofeed",
|
||||
"version": "v0.1.35",
|
||||
|
@ -3000,7 +3046,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/config",
|
||||
"version": "v2.8.24",
|
||||
"version": "v2.8.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/config.git",
|
||||
|
@ -3056,7 +3102,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/console",
|
||||
"version": "v2.8.24",
|
||||
"version": "v2.8.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/console.git",
|
||||
|
@ -3174,7 +3220,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/event-dispatcher",
|
||||
"version": "v2.8.24",
|
||||
"version": "v2.8.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/event-dispatcher.git",
|
||||
|
@ -3283,7 +3329,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/finder",
|
||||
"version": "v2.8.24",
|
||||
"version": "v2.8.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/finder.git",
|
||||
|
@ -3391,7 +3437,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/process",
|
||||
"version": "v2.8.24",
|
||||
"version": "v2.8.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/process.git",
|
||||
|
@ -3440,7 +3486,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/stopwatch",
|
||||
"version": "v2.8.24",
|
||||
"version": "v2.8.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/stopwatch.git",
|
||||
|
@ -3553,7 +3599,7 @@
|
|||
},
|
||||
{
|
||||
"name": "symfony/validator",
|
||||
"version": "v2.8.24",
|
||||
"version": "v2.8.25",
|
||||
"source": {
|
||||
"type": "git",
|
||||
"url": "https://github.com/symfony/validator.git",
|
||||
|
|
53
lib/CLI.php
Normal file
53
lib/CLI.php
Normal file
|
@ -0,0 +1,53 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse;
|
||||
|
||||
class CLI {
|
||||
protected $args = [];
|
||||
|
||||
protected function usage(): string {
|
||||
$prog = basename($_SERVER['argv'][0]);
|
||||
return <<<USAGE_TEXT
|
||||
Usage:
|
||||
$prog daemon
|
||||
$prog feed refresh <n>
|
||||
$prog --version
|
||||
$prog --help | -h
|
||||
|
||||
The Arsse command-line interface currently allows you to start the refresh
|
||||
daemon or refresh a specific feed by numeric ID.
|
||||
USAGE_TEXT;
|
||||
}
|
||||
|
||||
function __construct(array $argv = null) {
|
||||
if(is_null($argv)) {
|
||||
$argv = array_slice($_SERVER['argv'], 1);
|
||||
}
|
||||
$this->args = \Docopt::handle($this->usage(), [
|
||||
'argv' => $argv,
|
||||
'help' => true,
|
||||
'version' => VERSION,
|
||||
]);
|
||||
}
|
||||
|
||||
function dispatch(array $args = null): int {
|
||||
// act on command line
|
||||
if(is_null($args)) {
|
||||
$args = $this->args;
|
||||
}
|
||||
if($args['daemon']) {
|
||||
return $this->daemon();
|
||||
} elseif($args['feed'] && $args['refresh']) {
|
||||
return $this->feedRefresh((int) $args['<n>']);
|
||||
}
|
||||
}
|
||||
|
||||
protected function daemon(bool $loop = true): int {
|
||||
(new Service)->watch($loop);
|
||||
return 0; // FIXME: should return the exception code of thrown exceptions
|
||||
}
|
||||
|
||||
protected function feedRefresh(int $id): int {
|
||||
return (int) !Arsse::$db->feedUpdate($id);
|
||||
}
|
||||
}
|
17
lib/Conf.php
17
lib/Conf.php
|
@ -56,7 +56,7 @@ class Conf {
|
|||
public $userTempPasswordLength = 20;
|
||||
|
||||
/** @var string Class of the background feed update service driver in use (Forking by default) */
|
||||
public $serviceDriver = Service\Internal\Driver::class;
|
||||
public $serviceDriver = Service\Forking\Driver::class;
|
||||
/** @var string The interval between checks for new feeds, as an ISO 8601 duration
|
||||
* @see https://en.wikipedia.org/wiki/ISO_8601#Durations
|
||||
*/
|
||||
|
@ -84,7 +84,9 @@ class Conf {
|
|||
* @see self::importFile()
|
||||
*/
|
||||
public function __construct(string $import_file = "") {
|
||||
if($import_file != "") $this->importFile($import_file);
|
||||
if($import_file != "") {
|
||||
$this->importFile($import_file);
|
||||
}
|
||||
if(is_null($this->fetchUserAgentString)) {
|
||||
$this->fetchUserAgentString = sprintf('Arsse/%s (%s %s; %s; https://code.jkingweb.ca/jking/arsse) PicoFeed (https://github.com/fguillot/picoFeed)',
|
||||
VERSION, // Arsse version
|
||||
|
@ -100,8 +102,11 @@ class Conf {
|
|||
* The file must be a PHP script which return an array with keys that match the properties of the Conf class. Malformed files will throw an exception; unknown keys are silently ignored. Files may be imported is succession, though this is not currently used.
|
||||
* @param string $file Full path and file name for the file to import */
|
||||
public function importFile(string $file): self {
|
||||
if(!file_exists($file)) throw new Conf\Exception("fileMissing", $file);
|
||||
if(!is_readable($file)) throw new Conf\Exception("fileUnreadable", $file);
|
||||
if(!file_exists($file)) {
|
||||
throw new Conf\Exception("fileMissing", $file);
|
||||
} else if(!is_readable($file)) {
|
||||
throw new Conf\Exception("fileUnreadable", $file);
|
||||
}
|
||||
try {
|
||||
ob_start();
|
||||
$arr = (@include $file);
|
||||
|
@ -110,7 +115,9 @@ class Conf {
|
|||
} finally {
|
||||
ob_end_clean();
|
||||
}
|
||||
if(!is_array($arr)) throw new Conf\Exception("fileCorrupt", $file);
|
||||
if(!is_array($arr)) {
|
||||
throw new Conf\Exception("fileCorrupt", $file);
|
||||
}
|
||||
return $this->import($arr);
|
||||
}
|
||||
|
||||
|
|
|
@ -18,31 +18,34 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
|
||||
public function __construct(bool $install = false) {
|
||||
// check to make sure required extension is loaded
|
||||
if(!class_exists("SQLite3")) throw new Exception("extMissing", self::driverName());
|
||||
if(!class_exists("SQLite3")) {
|
||||
throw new Exception("extMissing", self::driverName());
|
||||
}
|
||||
$file = Arsse::$conf->dbSQLite3File;
|
||||
// if the file exists (or we're initializing the database), try to open it
|
||||
try {
|
||||
$this->db = $this->makeConnection($file, ($install) ? \SQLITE3_OPEN_READWRITE | \SQLITE3_OPEN_CREATE : \SQLITE3_OPEN_READWRITE, Arsse::$conf->dbSQLite3Key);
|
||||
} catch(\Throwable $e) {
|
||||
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
|
||||
if(!file_exists($file)) {
|
||||
if($install && !is_writable(dirname($file))) throw new Exception("fileUncreatable", dirname($file));
|
||||
throw new Exception("fileMissing", $file);
|
||||
}
|
||||
if(!is_readable($file) && !is_writable($file)) throw new Exception("fileUnusable", $file);
|
||||
if(!is_readable($file)) throw new Exception("fileUnreadable", $file);
|
||||
if(!is_writable($file)) throw new Exception("fileUnwritable", $file);
|
||||
// otherwise the database is probably corrupt
|
||||
throw new Exception("fileCorrupt", $mainfile);
|
||||
}
|
||||
try {
|
||||
$this->db = $this->makeConnection($file, \SQLITE3_OPEN_CREATE | \SQLITE3_OPEN_READWRITE, Arsse::$conf->dbSQLite3Key);
|
||||
// set initial options
|
||||
$this->db->enableExceptions(true);
|
||||
$this->exec("PRAGMA journal_mode = wal");
|
||||
$this->exec("PRAGMA foreign_keys = yes");
|
||||
} catch(\Exception $e) {
|
||||
list($excClass, $excMsg, $excData) = $this->exceptionBuild();
|
||||
throw new $excClass($excMsg, $excData);
|
||||
} catch(\Throwable $e) {
|
||||
// if opening the database doesn't work, check various pre-conditions to find out what the problem might be
|
||||
if(!file_exists($file)) {
|
||||
if($install && !is_writable(dirname($file))) {
|
||||
throw new Exception("fileUncreatable", dirname($file));
|
||||
}
|
||||
throw new Exception("fileMissing", $file);
|
||||
}
|
||||
if(!is_readable($file) && !is_writable($file)) {
|
||||
throw new Exception("fileUnusable", $file);
|
||||
} else if(!is_readable($file)) {
|
||||
throw new Exception("fileUnreadable", $file);
|
||||
} else if(!is_writable($file)) {
|
||||
throw new Exception("fileUnwritable", $file);
|
||||
}
|
||||
// otherwise the database is probably corrupt
|
||||
throw new Exception("fileCorrupt", $mainfile);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -66,8 +69,11 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
|
||||
public function schemaUpdate(int $to): bool {
|
||||
$ver = $this->schemaVersion();
|
||||
if(!Arsse::$conf->dbAutoUpdate) throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]);
|
||||
if($ver >= $to) throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
|
||||
if(!Arsse::$conf->dbAutoUpdate) {
|
||||
throw new Exception("updateManual", ['version' => $ver, 'driver_name' => $this->driverName()]);
|
||||
} else if($ver >= $to) {
|
||||
throw new Exception("updateTooNew", ['difference' => ($ver - $to), 'current' => $ver, 'target' => $to, 'driver_name' => $this->driverName()]);
|
||||
}
|
||||
$sep = \DIRECTORY_SEPARATOR;
|
||||
$path = Arsse::$conf->dbSchemaBase.$sep."SQLite3".$sep;
|
||||
// lock the database
|
||||
|
@ -76,16 +82,23 @@ class Driver extends \JKingWeb\Arsse\Db\AbstractDriver {
|
|||
$this->savepointCreate();
|
||||
try {
|
||||
$file = $path.$a.".sql";
|
||||
if(!file_exists($file)) throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||
if(!is_readable($file)) throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||
if(!file_exists($file)) {
|
||||
throw new Exception("updateFileMissing", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||
} else if(!is_readable($file)) {
|
||||
throw new Exception("updateFileUnreadable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||
}
|
||||
$sql = @file_get_contents($file);
|
||||
if($sql===false) throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||
if($sql===false) {
|
||||
throw new Exception("updateFileUnusable", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||
}
|
||||
try {
|
||||
$this->exec($sql);
|
||||
} catch(\Throwable $e) {
|
||||
throw new Exception("updateFileError", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a, 'message' => $this->getError()]);
|
||||
}
|
||||
if($this->schemaVersion() != $a+1) throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||
if($this->schemaVersion() != $a+1) {
|
||||
throw new Exception("updateFileIncomplete", ['file' => $file, 'driver_name' => $this->driverName(), 'current' => $a]);
|
||||
}
|
||||
} catch(\Throwable $e) {
|
||||
// undo any partial changes from the failed update
|
||||
$this->savepointUndo();
|
||||
|
|
21
lib/REST.php
21
lib/REST.php
|
@ -30,7 +30,7 @@ class REST {
|
|||
function __construct() {
|
||||
}
|
||||
|
||||
function dispatch(REST\Request $req = null): bool {
|
||||
function dispatch(REST\Request $req = null): REST\Response {
|
||||
if($req===null) {
|
||||
$req = new REST\Request();
|
||||
}
|
||||
|
@ -39,24 +39,7 @@ class REST {
|
|||
$req->refreshURL();
|
||||
$class = $this->apis[$api]['class'];
|
||||
$drv = new $class();
|
||||
$out = $drv->dispatch($req);
|
||||
header("Status: ".$out->code." ".Arsse::$lang->msg("HTTP.Status.".$out->code));
|
||||
if(!is_null($out->payload)) {
|
||||
header("Content-Type: ".$out->type);
|
||||
switch($out->type) {
|
||||
case REST\Response::T_JSON:
|
||||
$body = json_encode($out->payload,\JSON_PRETTY_PRINT);
|
||||
break;
|
||||
default:
|
||||
$body = (string) $out->payload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach($out->fields as $field) {
|
||||
header($field);
|
||||
}
|
||||
echo $body;
|
||||
return true;
|
||||
return $drv->dispatch($req);
|
||||
}
|
||||
|
||||
function apiMatch(string $url, array $map): string {
|
||||
|
|
6
lib/REST/Exception.php
Normal file
6
lib/REST/Exception.php
Normal file
|
@ -0,0 +1,6 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\REST;
|
||||
|
||||
class Exception extends \JKingWeb\Arsse\AbstractException {
|
||||
}
|
|
@ -1,6 +1,7 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\REST;
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
|
||||
class Response {
|
||||
const T_JSON = "application/json";
|
||||
|
@ -19,4 +20,28 @@ class Response {
|
|||
$this->type = $type;
|
||||
$this->fields = $extraFields;
|
||||
}
|
||||
|
||||
function output() {
|
||||
if(!headers_sent()) {
|
||||
header("Status: ".$this->code." ".Arsse::$lang->msg("HTTP.Status.".$this->code));
|
||||
$body = "";
|
||||
if(!is_null($this->payload)) {
|
||||
header("Content-Type: ".$this->type);
|
||||
switch($this->type) {
|
||||
case self::T_JSON:
|
||||
$body = json_encode($this->payload,\JSON_PRETTY_PRINT);
|
||||
break;
|
||||
default:
|
||||
$body = (string) $this->payload;
|
||||
break;
|
||||
}
|
||||
}
|
||||
foreach($this->fields as $field) {
|
||||
header($field);
|
||||
}
|
||||
echo $body;
|
||||
} else {
|
||||
throw new REST\Exception("headersSent");
|
||||
}
|
||||
}
|
||||
}
|
43
lib/Service/Forking/Driver.php
Normal file
43
lib/Service/Forking/Driver.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
declare(strict_types=1);
|
||||
namespace JKingWeb\Arsse\Service\Forking;
|
||||
use JKingWeb\Arsse\Arsse;
|
||||
|
||||
class Driver implements \JKingWeb\Arsse\Service\Driver {
|
||||
protected $queue = [];
|
||||
|
||||
static function driverName(): string {
|
||||
return Arsse::$lang->msg("Driver.Service.Forking.Name");
|
||||
}
|
||||
|
||||
static function requirementsMet(): bool {
|
||||
return function_exists("popen");
|
||||
}
|
||||
|
||||
function __construct() {
|
||||
}
|
||||
|
||||
function queue(int ...$feeds): int {
|
||||
$this->queue = array_merge($this->queue, $feeds);
|
||||
return sizeof($this->queue);
|
||||
}
|
||||
|
||||
function exec(): int {
|
||||
$pp = [];
|
||||
while($this->queue) {
|
||||
$id = (int) array_shift($this->queue);
|
||||
array_push($pp, popen('"'.\PHP_BINARY.'" "'.$_SERVER['argv'][0].'" feed refresh '.$id, "r"));
|
||||
}
|
||||
while($pp) {
|
||||
$p = array_pop($pp);
|
||||
fgets($p); // TODO: log output
|
||||
pclose($p);
|
||||
}
|
||||
return Arsse::$conf->serviceQueueWidth - sizeof($this->queue);
|
||||
}
|
||||
|
||||
function clean(): bool {
|
||||
$this->queue = [];
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -15,6 +15,9 @@
|
|||
<directory suffix=".php">../lib</directory>
|
||||
</whitelist>
|
||||
</filter>
|
||||
<logging>
|
||||
<log type="coverage-html" target="coverage" showUncoveredFiles="true"/>
|
||||
</logging>
|
||||
|
||||
<testsuites>
|
||||
<testsuite name="Exceptions">
|
||||
|
@ -62,6 +65,5 @@
|
|||
<testsuite name="Refresh service">
|
||||
<file>Service/TestService.php</file>
|
||||
</testsuite>
|
||||
|
||||
</testsuites>
|
||||
</phpunit>
|
Loading…
Reference in a new issue