nur-sery/nur_src/cli/Application.php

454 lines
13 KiB
PHP
Raw Normal View History

2023-12-03 22:10:18 +04:00
<?php
namespace nur\cli;
use Exception;
2024-07-16 01:54:30 +04:00
use nur\b\ExitError;
2023-12-03 22:10:18 +04:00
use nur\b\ValueException;
use nur\config;
use nur\config\ArrayConfig;
use nur\msg;
use nur\os;
use nur\path;
2024-09-30 08:41:47 +04:00
use nur\sery\app;
2024-09-30 10:45:54 +04:00
use nur\sery\app\RunFile;
use nur\sery\output\console as nconsole;
use nur\sery\output\log as nlog;
use nur\sery\output\msg as nmsg;
2024-06-11 15:56:54 +04:00
use nur\sery\output\std\StdMessenger;
2024-09-30 10:45:54 +04:00
use nur\yaml;
2023-12-03 22:10:18 +04:00
/**
* Class Application: application de base
*/
abstract class Application {
2024-06-11 12:06:54 +04:00
/** @var string répertoire du projet (celui qui contient composer.json */
const PROJDIR = null;
2024-06-13 17:19:58 +04:00
/**
* @var array répertoires vendor exprimés relativement à PROJDIR
*/
const VENDOR = null;
2024-06-11 12:06:54 +04:00
/**
* @var string code du projet, utilisé pour dériver le noms de certains des
* paramètres extraits de l'environnement, e.g XXX_DATADIR si le projet a pour
* code xxx
2024-09-23 17:18:00 +04:00
*
* si non définie, cette valeur est calculée automatiquement à partir de
* self::PROJDIR sans le suffixe "-app"
2024-06-11 12:06:54 +04:00
*/
const APPCODE = null;
2024-09-30 10:45:54 +04:00
/**
* @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;
2024-06-11 12:06:54 +04:00
/**
* @var string code de l'application, utilisé pour inférer le nom de certains
2024-09-23 17:18:00 +04:00
* fichiers spécifiques à l'application.
*
* si non définie, cette valeur est calculée automatiquement à partir de
* static::class
2024-06-11 12:06:54 +04:00
*/
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;
2024-06-11 15:56:54 +04:00
/** @var bool faut-il activer automatiquement l'écriture dans les logs */
const USE_LOGFILE = null;
2024-06-11 12:06:54 +04:00
/** @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;
2024-06-26 11:18:44 +04:00
/** @var bool faut-il installer le gestionnaire de signaux? */
2024-09-30 10:45:54 +04:00
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);
}
2024-06-26 11:18:44 +04:00
2023-12-03 22:10:18 +04:00
protected static function _app_init(): void {
config::set_fact(config::FACT_CLI_APP);
2024-05-03 08:07:18 +04:00
2024-06-11 15:56:54 +04:00
# avant que l'application soit configurée, configurer le mode debug
2023-12-03 22:10:18 +04:00
msg::set_messenger_class(Console::class);
msg::get()->setParametrableParams([
# En ligne de commande, on peut afficher les messages loggués
"display_log" => true,
]);
msg::get()->setLevels(msg::DEBUG_LEVELS, msg::DEBUG_LEVELS, msg::DEBUG);
2024-06-11 15:56:54 +04:00
2023-12-03 22:10:18 +04:00
# si un fichier nommé .default-profile-devel existe dans le répertoire de
# l'application ou du projet, alors le profil par défaut est devel
global $argv;
$homedir = os::homedir();
$projdir = path::abspath(path::dirname($argv[0]));
while (true) {
if (file_exists("$projdir/.default-profile-devel")) {
config::set_default_profile(config::DEVEL);
break;
}
# s'arrêter au répertoire du projet, ou à $HOMEDIR, ou à la racine
if (file_exists("$projdir/composer.json")) break;
if ($projdir == $homedir) break;
$projdir = path::dirname($projdir);
if ($projdir == "/") break;
}
2024-06-11 15:56:54 +04:00
2024-09-30 10:45:54 +04:00
app::init(static::class);
2024-06-11 15:56:54 +04:00
nmsg::set_messenger(new StdMessenger([
"min_level" => nmsg::DEBUG,
]));
2023-12-03 22:10:18 +04:00
}
protected static function _app_configure(Application $app): void {
config::configure(config::CONFIGURE_INITIAL_ONLY);
# revenir à la configuration par défaut une fois que la configuration
# initiale est faite
msg::get()->setLevels(msg::PRINT_LEVELS, msg::LOG_LEVELS, msg::TYPE_LEVELS);
2024-06-11 15:56:54 +04:00
$msgs = ["console" => new StdMessenger([
"min_level" => nmsg::NORMAL,
])];
if (static::USE_LOGFILE) {
$msgs["log"] = new StdMessenger([
"output" => app::get()->getLogfile(),
"min_level" => nmsg::MINOR,
"add_date" => true,
]);
}
nmsg::init($msgs);
2023-12-03 22:10:18 +04:00
$app->parseArgs();
config::configure();
}
protected static function _app_main(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));
}
static function run(?Application $app=null): void {
2024-06-11 12:06:54 +04:00
$unlock = false;
$stop = false;
2024-09-30 10:45:54 +04:00
$shutdown = function () use (&$unlock, &$stop) {
2024-06-26 11:18:44 +04:00
if ($unlock) {
app::get()->getRunfile()->release();
$unlock = false;
}
if ($stop) {
app::get()->getRunfile()->wfStop();
$stop = false;
}
};
2024-09-30 10:45:54 +04:00
register_shutdown_function($shutdown);
app::install_signal_handler(static::INSTALL_SIGNAL_HANDLER);
2023-12-03 22:10:18 +04:00
try {
static::_app_init();
2024-09-30 10:45:54 +04:00
$useRunfile = static::USE_RUNFILE;
$useRunlock = static::USE_RUNLOCK;
if ($useRunfile) {
2024-06-13 12:24:15 +04:00
$runfile = app::get()->getRunfile();
2024-09-30 10:45:54 +04:00
2024-06-11 12:06:54 +04:00
global $argc, $argv;
2024-09-30 10:45:54 +04:00
self::_manage_runfile($argc, $argv, $runfile);
if ($useRunlock && $runfile->warnIfLocked()) exit(app::EC_LOCKED);
2024-06-13 12:24:15 +04:00
$runfile->wfStart();
2024-06-11 12:06:54 +04:00
$stop = true;
if ($useRunlock) {
2024-06-13 12:24:15 +04:00
$runfile->lock();
2024-06-11 12:06:54 +04:00
$unlock = true;
}
}
2023-12-03 22:10:18 +04:00
if ($app === null) $app = new static();
static::_app_configure($app);
static::_app_main($app);
2024-07-16 01:54:30 +04:00
} catch (ExitError $e) {
2024-06-21 21:43:32 +04:00
if ($e->haveMessage()) msg::error($e);
2023-12-03 22:10:18 +04:00
exit($e->getCode());
} catch (Exception $e) {
msg::error($e);
exit(1);
}
}
/**
* sortir de l'application avec un code d'erreur, qui est 0 par défaut (i.e
* pas d'erreur)
*
2024-07-16 01:54:30 +04:00
* équivalent à lancer l'exception {@link ExitError}
2023-12-03 22:10:18 +04:00
*/
protected static final function exit(int $exitcode=0, $message=null) {
2024-07-16 01:54:30 +04:00
throw new ExitError($exitcode, $message);
2023-12-03 22:10:18 +04:00
}
/**
* sortir de l'application avec un code d'erreur, qui vaut 1 par défaut (i.e
* une erreur s'est produite)
*
2024-07-16 01:54:30 +04:00
* équivalent à lancer l'exception {@link ExitError}
2023-12-03 22:10:18 +04:00
*/
protected static final function die($message=null, int $exitcode=1) {
2024-07-16 01:54:30 +04:00
throw new ExitError($exitcode, $message);
2023-12-03 22:10:18 +04:00
}
const PROFILE_SECTION = [
"title" => "PROFILS D'EXECUTION",
["group",
["-p", "--profile", "--app-profile",
"args" => 1, "argsdesc" => "PROFILE",
2023-12-03 22:10:18 +04:00
"action" => [null, "set_application_profile"],
"help" => "spécifier le profil d'exécution",
],
["-P", "--prod", "action" => [config::class, "set_profile", config::PROD]],
["-T", "--test", "action" => [config::class, "set_profile", config::TEST]],
["--devel", "action" => [config::class, "set_profile", config::DEVEL]],
],
];
static function set_application_profile(string $profile): void {
config::set_profile($profile);
}
const VERBOSITY_SECTION = [
"title" => "NIVEAU D'INFORMATION",
"show" => false,
["group",
["--verbosity",
"args" => 1, "argsdesc" => "silent|very-quiet|quiet|verbose|debug|trace",
2023-12-03 22:10:18 +04:00
"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"]],
["--sql-trace", "action" => [null, "set_application_sql_trace"]],
],
["-L", "--logfile",
"args" => "file", "argsdesc" => "OUTPUT",
2023-12-03 22:10:18 +04:00
"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]],
2023-12-03 22:10:18 +04:00
],
];
static function set_application_verbosity(string $verbosity): void {
$msg = msg::get();
2024-06-11 15:56:54 +04:00
$nconsole = nconsole::get();
2023-12-03 22:10:18 +04:00
switch ($verbosity) {
case "s":
case "silent":
$msg->setLevels([
msg::USER => msg::NEVER,
msg::TECH => msg::NEVER,
msg::EXCEPTION => msg::NEVER,
]);
2024-06-11 15:56:54 +04:00
$nconsole->resetParams([
"min_level" => nmsg::NONE,
]);
2023-12-03 22:10:18 +04:00
break;
case "Q":
case "very-quiet":
$msg->setLevels([
msg::USER => msg::CRITICAL,
msg::TECH => msg::CRITICAL,
msg::EXCEPTION => msg::NEVER,
]);
2024-06-11 15:56:54 +04:00
$nconsole->resetParams([
"min_level" => nmsg::MAJOR,
]);
2023-12-03 22:10:18 +04:00
break;
case "q":
case "quiet":
$msg->setLevels([
msg::USER => msg::MAJOR,
msg::TECH => msg::MAJOR,
msg::EXCEPTION => msg::NEVER,
]);
2024-06-11 15:56:54 +04:00
$nconsole->resetParams([
"min_level" => nmsg::MAJOR,
]);
2023-12-03 22:10:18 +04:00
break;
case "v":
case "verbose":
$msg->setLevels([
msg::USER => msg::MINOR,
msg::TECH => msg::MINOR,
msg::EXCEPTION => msg::NEVER,
]);
2024-06-11 15:56:54 +04:00
$nconsole->resetParams([
"min_level" => nmsg::MINOR,
]);
2023-12-03 22:10:18 +04:00
break;
case "D":
case "debug":
config::set_debug();
$msg->setLevels([
msg::USER => msg::MINOR,
msg::TECH => msg::MINOR,
msg::EXCEPTION => msg::NORMAL,
], null, msg::DEBUG);
2024-06-11 15:56:54 +04:00
$nconsole->resetParams([
"min_level" => nmsg::DEBUG,
]);
2023-12-03 22:10:18 +04:00
break;
case "T":
case "trace":
config::set_debug();
$msg->setLevels([
msg::USER => msg::MINOR,
msg::TECH => msg::MINOR,
msg::EXCEPTION => msg::MINOR,
], null, msg::DEBUG);
2024-06-11 15:56:54 +04:00
$nconsole->resetParams([
"min_level" => nmsg::DEBUG,
]);
2023-12-03 22:10:18 +04:00
break;
default:
throw ValueException::invalid_value($verbosity, "verbosity");
}
}
static function set_application_sql_trace(): void {
config::add(new ArrayConfig(["app" => ["trace_sql" => true]]));
}
static function set_application_log_output(string $logfile): void {
msg::get()->setParametrableParams(["log_output" => $logfile]);
2024-05-02 23:29:18 +04:00
nlog::create_or_reset_params([
"output" => $logfile,
2024-06-11 15:56:54 +04:00
], StdMessenger::class, [
2024-05-02 23:29:18 +04:00
"add_date" => true,
2024-05-03 08:07:18 +04:00
"min_level" => nlog::MINOR,
2024-05-02 23:29:18 +04:00
]);
2023-12-03 22:10:18 +04:00
}
static function set_application_color(bool $color): void {
2023-12-03 22:10:18 +04:00
msg::get()->setParametrableParams(["color" => $color]);
2024-06-11 15:56:54 +04:00
nconsole::reset_params([
"color" => $color,
]);
2023-12-03 22:10:18 +04:00
}
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 profile(?string $profile=null): string {
if ($profile === null) $profile = config::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();
}