378 lines
10 KiB
PHP
378 lines
10 KiB
PHP
<?php
|
|
namespace nur\sery\app\cli;
|
|
|
|
use Exception;
|
|
use nur\cli\ArgsException;
|
|
use nur\cli\ArgsParser;
|
|
use nur\config;
|
|
use nur\sery\app\app;
|
|
use nur\sery\app\RunFile;
|
|
use nur\sery\ExitError;
|
|
use nur\sery\output\console;
|
|
use nur\sery\output\log;
|
|
use nur\sery\output\msg;
|
|
use nur\sery\output\std\StdMessenger;
|
|
use nur\sery\ValueException;
|
|
use nur\yaml;
|
|
|
|
/**
|
|
* Class Application: application de base
|
|
*/
|
|
abstract class Application {
|
|
/** @var string répertoire du projet (celui qui contient composer.json */
|
|
const PROJDIR = null;
|
|
|
|
/**
|
|
* @var array répertoires vendor exprimés relativement à PROJDIR
|
|
*
|
|
* les clés suivantes doivent être présentes dans le tableau:
|
|
* - autoload (chemin vers vendor/autoload.php)
|
|
* - bindir (chemin vers vendor/bin)
|
|
*/
|
|
const VENDOR = null;
|
|
|
|
/**
|
|
* @var string code du projet, utilisé pour dériver le noms de certains des
|
|
* paramètres extraits de l'environnement, e.g XXX_YYY_DATADIR si le projet a
|
|
* pour code xxx-yyy
|
|
*
|
|
* si non définie, cette valeur est calculée automatiquement à partir de
|
|
* self::PROJDIR sans le suffixe "-app"
|
|
*/
|
|
const APPCODE = null;
|
|
|
|
/**
|
|
* @var string|null identifiant d'un groupe auquel l'application appartient.
|
|
* les applications du même groupe enregistrent leur fichiers de controle au
|
|
* même endroit $VARDIR/$APPGROUP
|
|
*/
|
|
const APPGROUP = null;
|
|
|
|
/**
|
|
* @var string code de l'application, utilisé pour inférer le nom de certains
|
|
* fichiers spécifiques à l'application.
|
|
*
|
|
* si non définie, cette valeur est calculée automatiquement à partir de
|
|
* static::class
|
|
*/
|
|
const NAME = null;
|
|
|
|
/** @var string description courte de l'application */
|
|
const TITLE = null;
|
|
|
|
const DATADIR = null;
|
|
const ETCDIR = null;
|
|
const VARDIR = null;
|
|
const LOGDIR = null;
|
|
|
|
/** @var bool faut-il activer automatiquement l'écriture dans les logs */
|
|
const USE_LOGFILE = null;
|
|
|
|
/** @var bool faut-il maintenir un fichier de suivi du process? */
|
|
const USE_RUNFILE = false;
|
|
|
|
/**
|
|
* @var bool faut-il empêcher deux instances de cette application de se lancer
|
|
* en même temps?
|
|
*
|
|
* nécessite USE_RUNFILE==true
|
|
*/
|
|
const USE_RUNLOCK = false;
|
|
|
|
/** @var bool faut-il installer le gestionnaire de signaux? */
|
|
const INSTALL_SIGNAL_HANDLER = false;
|
|
|
|
private static function _info(string $message, int $ec=0): int {
|
|
fwrite(STDERR, "INFO: $message\n");
|
|
return $ec;
|
|
}
|
|
|
|
private static function _error(string $message, int $ec=1): int {
|
|
fwrite(STDERR, "ERROR: $message\n");
|
|
return $ec;
|
|
}
|
|
|
|
static function _manage_runfile(int &$argc, array &$argv, RunFile $runfile): void {
|
|
if ($argc <= 1 || $argv[1] !== "//") return;
|
|
array_splice($argv, 1, 1); $argc--;
|
|
$ec = 0;
|
|
switch ($argv[1] ?? "infos") {
|
|
case "help":
|
|
self::_info(<<<EOT
|
|
Valid commands:
|
|
infos
|
|
dump
|
|
reset
|
|
release
|
|
start
|
|
kill
|
|
|
|
EOT);
|
|
break;
|
|
case "infos":
|
|
case "i":
|
|
$desc = $runfile->getDesc();
|
|
if ($runfile->isRunning()) {
|
|
$actionDesc = $runfile->getActionDesc();
|
|
if ($actionDesc !== null) $actionDesc = "\n$actionDesc";
|
|
echo "$desc$actionDesc\n";
|
|
} else {
|
|
echo "$desc\n";
|
|
$ec = 1;
|
|
}
|
|
break;
|
|
case "dump":
|
|
case "d":
|
|
yaml::dump($runfile->read());
|
|
break;
|
|
case "reset":
|
|
case "z":
|
|
if (!$runfile->isRunning()) $runfile->reset();
|
|
else $ec = self::_error("cannot reset while running");
|
|
break;
|
|
case "release":
|
|
case "rl":
|
|
$runfile->release();
|
|
break;
|
|
case "start":
|
|
case "s":
|
|
array_splice($argv, 1, 1); $argc--;
|
|
return;
|
|
case "kill":
|
|
case "k":
|
|
if ($runfile->isRunning()) $runfile->wfKill();
|
|
else $ec = self::_error("not running");
|
|
break;
|
|
default:
|
|
$ec = self::_error("$argv[1]: unexpected command", app::EC_BAD_COMMAND);
|
|
}
|
|
exit($ec);
|
|
}
|
|
|
|
static function run(?Application $app=null): void {
|
|
$unlock = false;
|
|
$stop = false;
|
|
$shutdown = function () use (&$unlock, &$stop) {
|
|
if ($unlock) {
|
|
app::get()->getRunfile()->release();
|
|
$unlock = false;
|
|
}
|
|
if ($stop) {
|
|
app::get()->getRunfile()->wfStop();
|
|
$stop = false;
|
|
}
|
|
};
|
|
register_shutdown_function($shutdown);
|
|
app::install_signal_handler(static::INSTALL_SIGNAL_HANDLER);
|
|
try {
|
|
static::_initialize_app();
|
|
$useRunfile = static::USE_RUNFILE;
|
|
$useRunlock = static::USE_RUNLOCK;
|
|
if ($useRunfile) {
|
|
$runfile = app::get()->getRunfile();
|
|
|
|
global $argc, $argv;
|
|
self::_manage_runfile($argc, $argv, $runfile);
|
|
if ($useRunlock && $runfile->warnIfLocked()) exit(app::EC_LOCKED);
|
|
|
|
$runfile->wfStart();
|
|
$stop = true;
|
|
if ($useRunlock) {
|
|
$runfile->lock();
|
|
$unlock = true;
|
|
}
|
|
}
|
|
if ($app === null) $app = new static();
|
|
static::_configure_app($app);
|
|
static::_start_app($app);
|
|
} catch (ExitError $e) {
|
|
if ($e->haveUserMessage()) msg::error($e->getUserMessage());
|
|
exit($e->getCode());
|
|
} catch (Exception $e) {
|
|
msg::error($e);
|
|
exit(app::EC_UNEXPECTED);
|
|
}
|
|
}
|
|
|
|
protected static function _initialize_app(): void {
|
|
app::init(static::class);
|
|
msg::set_messenger(new StdMessenger([
|
|
"min_level" => msg::DEBUG,
|
|
]));
|
|
}
|
|
|
|
protected static function _configure_app(Application $app): void {
|
|
config::configure(config::CONFIGURE_INITIAL_ONLY);
|
|
|
|
$msgs = null;
|
|
$msgs["console"] = new StdMessenger([
|
|
"min_level" => msg::NORMAL,
|
|
]);
|
|
if (static::USE_LOGFILE) {
|
|
$msgs["log"] = new StdMessenger([
|
|
"output" => app::get()->getLogfile(),
|
|
"min_level" => msg::MINOR,
|
|
"add_date" => true,
|
|
]);
|
|
}
|
|
msg::init($msgs);
|
|
|
|
$app->parseArgs();
|
|
config::configure();
|
|
}
|
|
|
|
protected static function _start_app(Application $app): void {
|
|
$retcode = $app->main();
|
|
if (is_int($retcode)) exit($retcode);
|
|
elseif (is_bool($retcode)) exit($retcode? 0: 1);
|
|
elseif ($retcode !== null) exit(strval($retcode));
|
|
}
|
|
|
|
/**
|
|
* sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e
|
|
* pas d'erreur)
|
|
*
|
|
* équivalent à lancer l'exception {@link ExitError}
|
|
*/
|
|
protected static final function exit(int $exitcode=0, $message=null) {
|
|
throw new ExitError($exitcode, $message);
|
|
}
|
|
|
|
/**
|
|
* sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e
|
|
* une erreur s'est produite)
|
|
*
|
|
* équivalent à lancer l'exception {@link ExitError}
|
|
*/
|
|
protected static final function die($message=null, int $exitcode=1) {
|
|
throw new ExitError($exitcode, $message);
|
|
}
|
|
|
|
const PROFILE_SECTION = [
|
|
"title" => "PROFILS D'EXECUTION",
|
|
["group",
|
|
["-p", "--profile", "--app-profile",
|
|
"args" => 1, "argsdesc" => "PROFILE",
|
|
"action" => [app::class, "set_profile"],
|
|
"help" => "spécifier le profil d'exécution",
|
|
],
|
|
["-P", "--prod", "action" => [app::class, "set_profile", config::PROD]],
|
|
["-T", "--test", "action" => [app::class, "set_profile", config::TEST]],
|
|
["--devel", "action" => [app::class, "set_profile", config::DEVEL]],
|
|
],
|
|
];
|
|
|
|
const VERBOSITY_SECTION = [
|
|
"title" => "NIVEAU D'INFORMATION",
|
|
"show" => false,
|
|
["group",
|
|
["--verbosity",
|
|
"args" => 1, "argsdesc" => "silent|quiet|verbose|debug",
|
|
"action" => [null, "set_application_verbosity"],
|
|
"help" => "spécifier le niveau d'informations affiché",
|
|
],
|
|
["-q", "--quiet", "action" => [null, "set_application_verbosity", "quiet"]],
|
|
["-v", "--verbose", "action" => [null, "set_application_verbosity", "verbose"]],
|
|
["-D", "--debug", "action" => [null, "set_application_verbosity", "debug"]],
|
|
],
|
|
["-L", "--logfile",
|
|
"args" => "file", "argsdesc" => "OUTPUT",
|
|
"action" => [null, "set_application_log_output"],
|
|
"help" => "Logger les messages de l'application dans le fichier spécifié",
|
|
],
|
|
["group",
|
|
["--color",
|
|
"action" => [null, "set_application_color", true],
|
|
"help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut",
|
|
],
|
|
["--no-color", "action" => [null, "set_application_color", false]],
|
|
],
|
|
];
|
|
|
|
static function set_application_verbosity(string $verbosity): void {
|
|
$console = console::get();
|
|
switch ($verbosity) {
|
|
case "Q":
|
|
case "silent":
|
|
$console->resetParams([
|
|
"min_level" => msg::NONE,
|
|
]);
|
|
break;
|
|
case "q":
|
|
case "quiet":
|
|
$console->resetParams([
|
|
"min_level" => msg::MAJOR,
|
|
]);
|
|
break;
|
|
case "n":
|
|
case "normal":
|
|
$console->resetParams([
|
|
"min_level" => msg::NORMAL,
|
|
]);
|
|
break;
|
|
case "v":
|
|
case "verbose":
|
|
$console->resetParams([
|
|
"min_level" => msg::MINOR,
|
|
]);
|
|
break;
|
|
case "D":
|
|
case "debug":
|
|
config::set_debug();
|
|
$console->resetParams([
|
|
"min_level" => msg::DEBUG,
|
|
]);
|
|
break;
|
|
default:
|
|
throw ValueException::invalid_value($verbosity, "verbosity");
|
|
}
|
|
}
|
|
|
|
static function set_application_log_output(string $logfile): void {
|
|
log::create_or_reset_params([
|
|
"output" => $logfile,
|
|
], StdMessenger::class, [
|
|
"add_date" => true,
|
|
"min_level" => log::MINOR,
|
|
]);
|
|
}
|
|
static function set_application_color(bool $color): void {
|
|
console::reset_params([
|
|
"color" => $color,
|
|
]);
|
|
}
|
|
const ARGS = [
|
|
"sections" => [
|
|
self::PROFILE_SECTION,
|
|
self::VERBOSITY_SECTION,
|
|
],
|
|
];
|
|
|
|
/** @throws ArgsException */
|
|
function parseArgs(array $args=null): void {
|
|
$parser = new ArgsParser(static::ARGS);
|
|
$parser->parse($this, $args);
|
|
}
|
|
|
|
const PROFILE_COLORS = [
|
|
"prod" => "@r",
|
|
"test" => "@g",
|
|
"devel" => "@w",
|
|
];
|
|
const DEFAULT_PROFILE_COLOR = "y";
|
|
|
|
/** retourner le profil courant en couleur */
|
|
static function get_profile(?string $profile=null): string {
|
|
if ($profile === null) $profile = app::get_profile();
|
|
foreach (static::PROFILE_COLORS as $text => $color) {
|
|
if (strpos($profile, $text) !== false) {
|
|
return $color? "<color $color>$profile</color>": $profile;
|
|
}
|
|
}
|
|
$color = static::DEFAULT_PROFILE_COLOR;
|
|
return $color? "<color $color>$profile</color>": $profile;
|
|
}
|
|
|
|
abstract function main();
|
|
}
|