Browse Source

Experimental forking service and accompanying CLI

- Improves #48, #57, and #61
microsub
J. King 7 years ago
parent
commit
1b970cc7c5
  1. 15
      arsse.php
  2. 3
      composer.json
  3. 62
      composer.lock
  4. 53
      lib/CLI.php
  5. 17
      lib/Conf.php
  6. 55
      lib/Db/SQLite3/Driver.php
  7. 21
      lib/REST.php
  8. 6
      lib/REST/Exception.php
  9. 25
      lib/REST/Response.php
  10. 43
      lib/Service/Forking/Driver.php
  11. 4
      tests/phpunit.xml

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();
}

3
composer.json

@ -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

@ -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

@ -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

@ -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);
}

55
lib/Db/SQLite3/Driver.php

@ -18,32 +18,35 @@ 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);
$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(\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));
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);
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);
}
try {
// 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);
}
}
protected function makeConnection(string $file, int $opts, string $key) {
@ -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

@ -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

@ -0,0 +1,6 @@
<?php
declare(strict_types=1);
namespace JKingWeb\Arsse\REST;
class Exception extends \JKingWeb\Arsse\AbstractException {
}

25
lib/REST/Response.php

@ -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

@ -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;
}
}

4
tests/phpunit.xml

@ -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…
Cancel
Save