modifs.mineures sans commentaires

This commit is contained in:
Jephté Clain 2024-09-25 17:10:14 +04:00
parent ccb81d35a6
commit 51f8ae487a
18 changed files with 763 additions and 2046 deletions

View File

@ -2,6 +2,6 @@
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use nur\tools\SteamTrainApp;
use nur\sery\wip\tools\SteamTrainApp;
SteamTrainApp::run();

View File

@ -1,24 +0,0 @@
<?php
namespace nur\cli;
use nur\sery\wip\app\app2;
class BgApplication2 extends Application2 {
const USE_LOGFILE = true;
const USE_RUNFILE = true;
const USE_RUNLOCK = true;
const ARGS = [
"merge" => parent::ARGS,
["--force-enabled", "value" => true,
"help" => "lancer la commande même si les tâches planifiées sont désactivées",
],
];
protected bool $forceEnabled = false;
function main() {
app2::check_bgapplication_enabled($this->forceEnabled);
}
}

View File

@ -1,23 +0,0 @@
<?php
namespace nur\tools;
use nur\cli\BgApplication2;
use nur\sery\output\msg;
class SteamTrainApp extends BgApplication2 {
const NAME = self::class;
const TITLE = "Train à vapeur";
//const USE_SIGNAL_HANDLER = true;
const ARGS = [
"merge_arrays" => [BgApplication2::ARGS, parent::ARGS],
"purpose" => self::TITLE,
];
function main() {
for ($i = 1; $i <= 100; $i++) {
msg::print("Tchou-tchou! x $i");
sleep(1);
}
}
}

37
src/app/cli/cli_cli_wrapper.sh Executable file
View File

@ -0,0 +1,37 @@
#!/bin/bash
# -*- coding: utf-8 mode: sh -*- vim:sw=4:sts=4:et:ai:si:sta:fenc=utf-8
# s'assurer que le script PHP est lancé avec l'utilisateur www-data
if [ ! -L "$0" ]; then
echo "\
$0: ce script
- doit être lancé en tant que lien symbolique avec un nom de la forme 'monscript.php'
- lance le script PHP du même nom situé dans le même répertoire avec l'utilisateur www-data"
exit 0
fi
MYNAME="$(basename -- "$0")"
MYTRUESELF="$(readlink -f "$0")"
MYTRUEDIR="$(dirname -- "$MYTRUESELF")"
PROJDIR="$(cd "$MYTRUEDIR/.."; pwd)"
www_data="${DEVUSER_USERENT%%:*}"
[ -n "$www_data" ] || www_data=www-data
class="$MYTRUEDIR/${MYNAME%.php}.phpc"
script="$MYTRUEDIR/${MYNAME%.php}.php"
cmd=(php "$PROJDIR/src_app/init/cli_cli_launcher.php")
[ -n "$MEMPROF_PROFILE" ] && cmd+=(-dextension=memprof.so)
if [ -f "$class" ]; then
cmd+=("$(<"$class")")
else
cmd+=("$script")
fi
cmd+=("$@")
if [ "$(id -u)" -eq 0 ]; then
su-exec "$www_data" "${cmd[@]}"
else
exec "${cmd[@]}"
fi

View File

@ -5,7 +5,7 @@ use nur\sery\output\std\ProxyMessenger;
use nur\sery\php\nur_func;
/**
* Class msg: inscrire un message dans les logs ET l'afficher sur la console
* Class msg: inscrire un message dans les logs ET l'afficher à l'utilisateur
*
* Cette classe DOIT être initialisée avec {@link set_messenger()} ou
* {@link set_messenger_class()} avant d'être utilisée.

89
wip/app/LockFile.php Normal file
View File

@ -0,0 +1,89 @@
<?php
namespace nur\sery\wip\app;
use nur\sery\cl;
use nur\sery\file\SharedFile;
use nur\sery\output\msg;
use nur\sery\php\time\DateTime;
/**
* Class LockFile: une classe qui permet à une application de verrouiller
* certaines actions
*/
class LockFile {
const NAME = null;
const TITLE = null;
function __construct($file, ?string $name=null, ?string $title=null) {
$this->file = new SharedFile($file);
$this->name = $name ?? static::NAME;
$this->title = $title ?? static::TITLE;
}
/** @var SharedFile */
protected $file;
/** @var ?string */
protected $name;
/** @var ?string */
protected $title;
protected function initData(): array {
return [
"name" => $this->name,
"title" => $this->title,
"locked" => false,
"date_lock" => null,
"date_release" => null,
];
}
function read(bool $close=true): array {
$data = $this->file->unserialize(null, $close);
if (!is_array($data)) $data = $this->initData();
return $data;
}
function isLocked(?array &$data=null): bool {
$data = $this->read();
return $data["locked"];
}
function warnIfLocked(?array $data=null): bool {
if ($data === null) $data = $this->read();
if ($data["locked"]) {
msg::warning("$data[name]: possède le verrou depuis $data[date_lock] -- $data[title]");
return true;
}
return false;
}
function lock(?array &$data=null): bool {
$file = $this->file;
$data = $this->read(false);
if ($data["locked"]) {
$file->close();
return false;
} else {
$file->ftruncate();
$file->serialize(cl::merge($data, [
"locked" => true,
"date_lock" => new DateTime(),
"date_release" => null,
]));
return true;
}
}
function release(?array &$data=null): void {
$file = $this->file;
$data = $this->read(false);
$file->ftruncate();
$file->serialize(cl::merge($data, [
"locked" => false,
"date_release" => new DateTime(),
]));
}
}

375
wip/app/RunFile.php Normal file
View File

@ -0,0 +1,375 @@
<?php
namespace nur\sery\wip\app;
use nur\sery\cl;
use nur\sery\file\SharedFile;
use nur\sery\os\path;
use nur\sery\output\msg;
use nur\sery\php\time\DateTime;
use nur\sery\str;
/**
* Class RunFile: une classe permettant de suivre le fonctionnement d'une
* application qui tourne en tâche de fond
*/
class RunFile {
const RUN_EXT = ".run";
const LOCK_EXT = ".lock";
const NAME = null;
function __construct(?string $name, string $file, ?string $logfile=null) {
$file = path::ensure_ext($file, self::RUN_EXT);
$this->name = $name ?? static::NAME;
$this->file = new SharedFile($file);
$this->logfile = $logfile;
}
protected ?string $name;
protected SharedFile $file;
protected ?string $logfile;
function getLogfile(): ?string {
return $this->logfile;
}
protected static function merge(array $data, array $merge): array {
return cl::merge($data, [
"serial" => $data["serial"] + 1,
], $merge);
}
protected function initData(bool $forStart=true): array {
if ($forStart) {
$pid = posix_getpid();
$dateStart = new DateTime();
} else {
$pid = $dateStart = null;
}
return [
"name" => $this->name,
"id" => bin2hex(random_bytes(16)),
"pg_pid" => null,
"pid" => $pid,
"serial" => 0,
# lock
"locked" => false,
"date_lock" => null,
"date_release" => null,
# run
"logfile" => $this->logfile,
"date_start" => $dateStart,
"date_stop" => null,
"exitcode" => null,
"is_done" => null,
# action
"action" => null,
"action_date_start" => null,
"action_current_step" => null,
"action_max_step" => null,
"action_date_step" => null,
];
}
function read(): array {
$data = $this->file->unserialize();
if (!is_array($data)) $data = $this->initData(false);
return $data;
}
protected function willWrite(): array {
$file = $this->file;
$file->lockWrite();
$data = $file->unserialize(null, false, true);
if (!is_array($data)) {
$data = $this->initData(false);
$file->ftruncate();
$file->serialize($data, false, true);
}
return [$file, $data];
}
protected function serialize(SharedFile $file, array $data, ?array $merge=null): void {
$file->ftruncate();
$file->serialize(self::merge($data, $merge), true, true);
}
protected function update(callable $func): void {
/** @var SharedFile$file */
[$file, $data] = $this->willWrite();
$merge = call_user_func($func, $data);
if ($merge !== null && $merge !== false) {
$this->serialize($file, $data, $merge);
} else {
$file->cancelWrite();
}
}
function haveWorked(int $serial, ?int &$currentSerial=null, ?array $data=null): bool {
$data ??= $this->read();
$currentSerial = $data["serial"];
return $serial !== $currentSerial;
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# verrouillage par défaut
function isLocked(?array &$data=null): bool {
$data = $this->read();
return $data["locked"];
}
function warnIfLocked(?array $data=null): bool {
$data ??= $this->read();
if ($data["locked"]) {
msg::warning("$data[name]: possède le verrou depuis $data[date_lock]");
return true;
}
return false;
}
function lock(): bool {
$this->update(function ($data) use (&$locked) {
if ($data["locked"]) {
$locked = false;
return null;
} else {
$locked = true;
return [
"locked" => true,
"date_lock" => new DateTime(),
"date_release" => null,
];
}
});
return $locked;
}
function release(): void {
$this->update(function ($data) {
return [
"locked" => false,
"date_release" => new DateTime(),
];
});
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# cycle de vie de l'application
/**
* indiquer que l'application démarre. l'état est entièrement réinitialisé,
* sauf le PID du leader qui est laissé en l'état
*/
function wfStart(): void {
$this->update(function (array $data) {
return cl::merge($this->initData(), [
"pg_pid" => $data["pg_pid"],
]);
});
}
/** tester si l'application a déjà été démarrée au moins une fois */
function wasStarted(?array $data=null): bool {
$data ??= $this->read();
return $data["date_start"] !== null;
}
/** tester si l'application est démarrée et non arrêtée */
function isStarted(?array $data=null): bool {
$data ??= $this->read();
return $data["date_start"] !== null && $data["date_stop"] === null;
}
/**
* vérifier si l'application marquée comme démarrée tourne réellement
*/
function isRunning(?array $data=null): bool {
$data ??= $this->read();
if ($data["date_start"] === null) return false;
if ($data["date_stop"] !== null) return false;
if (!posix_kill($data["pid"], 0)) {
switch (posix_get_last_error()) {
case 1: #PCNTL_EPERM:
# process auquel on n'a pas accès?! est-ce un autre process qui a
# réutilisé le PID?
return false;
case 3: #PCNTL_ESRCH:
# process inexistant
return false;
case 22: #PCNTL_EINVAL:
# ne devrait pas se produire
return false;
}
}
# process existant auquel on a accès
return true;
}
/** indiquer que l'application s'arrête */
function wfStop(): void {
$this->update(function (array $data) {
return ["date_stop" => new DateTime()];
});
}
/** tester si l'application est déjà été stoppée au moins une fois */
function wasStopped(?array $data=null): bool {
$data ??= $this->read();
return $data["date_stop"] !== null;
}
/** tester si l'application a été démarrée puis arrêtée */
function isStopped(?array $data=null): bool {
$data ??= $this->read();
return $data["date_start"] !== null && $data["date_stop"] !== null;
}
/** après l'arrêt de l'application, mettre à jour le code de retour */
function wfStopped(int $exitcode): void {
$this->update(function (array $data) use ($exitcode) {
return [
"pg_pid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $exitcode,
];
});
}
/**
* comme {@link self::isStopped()} mais ne renvoie true qu'une seule fois si
* $updateDone==true
*/
function isDone(?array &$data=null, bool $updateDone=true): bool {
$done = false;
$this->update(function (array $ldata) use (&$done, &$data, $updateDone) {
$data = $ldata;
if ($data["date_start"] === null || $data["date_stop"] === null || $data["is_done"]) {
return false;
}
$done = true;
if ($updateDone) return ["is_done" => $done];
else return null;
});
return $done;
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# gestion des actions
/** indiquer le début d'une action */
function action(?string $title, ?int $maxSteps=null): void {
$this->update(function (array $data) use ($title, $maxSteps) {
return [
"action" => $title,
"action_date_start" => new DateTime(),
"action_max_step" => $maxSteps,
"action_current_step" => 0,
];
});
}
/** indiquer qu'une étape est franchie dans l'action en cours */
function step(int $nbSteps=1): void {
$this->update(function (array $data) use ($nbSteps) {
return [
"action_date_step" => new DateTime(),
"action_current_step" => $data["action_current_step"] + $nbSteps,
];
});
app2::_dispatch_signals();
}
function getActionDesc(?array $data=null): ?string {
$data ??= $this->read();
$action = $data["action"];
if ($action === null) {
return "pid $data[pid] [$data[date_start]]";
}
$date ??= $data["action_date_step"];
$date ??= $data["action_date_start"];
if ($date !== null) $action = "[$date] $action";
$current = $data["action_current_step"];
$max = $data["action_max_step"];
if ($current !== null && $max !== null) {
$action .= " ($current / $max)";
} elseif ($current !== null) {
$action .= " ($current)";
}
return "pid $data[pid] $action";
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Divers
function getLockFile(?string $name=null, ?string $title=null): LockFile {
$ext = self::LOCK_EXT;
if ($name !== null) $ext = ".$name$ext";
$file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT);
$name = str::join("/", [$this->name, $name]);
return new LockFile($file, $name, $title);
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# Gestionnaire de tâches (tm_*)
/** démarrer un groupe de process dont le process courant est le leader */
function tm_startPg(): void {
$this->update(function (array $data) {
posix_setsid();
return [
"pg_pid" => posix_getpid(),
];
});
}
/**
* vérifier si on est dans le cas la tâche devrait tourner mais en réalité
* ce n'est pas le cas
*/
function tm_isUndead(?int $pid=null): bool {
$data = $this->read();
if ($data["date_start"] === null) return false;
if ($data["date_stop"] !== null) return false;
$pid ??= $data["pid"];
if (!posix_kill($pid, 0)) {
switch (posix_get_last_error()) {
case 1: #PCNTL_EPERM:
# process auquel on n'a pas accès?! est-ce un autre process qui a
# réutilisé le PID?
return false;
case 3: #PCNTL_ESRCH:
# process inexistant
return true;
case 22: #PCNTL_EINVAL:
# ne devrait pas se produire
return false;
}
}
# process existant auquel on a accès
return false;
}
function tm_isReapable(): bool {
$data = $this->read();
return $data["date_stop"] !== null && $data["exitcode"] === null;
}
/** marquer la tâche comme terminée */
function tm_reap(?int $pid=null): void {
$data = $this->read();
$pid ??= $data["pid"];
pcntl_waitpid($pid, $status);
$exitcode = pcntl_wifexited($status)? pcntl_wexitstatus($status): 127;
$this->update(function (array $data) use ($exitcode) {
return [
"pg_pid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $data["exitcode"] ?? $exitcode,
];
});
}
}

View File

@ -1,20 +1,18 @@
<?php
namespace nur\sery\wip\app;
use nur\b\ExitError;
use nur\cli\Application;
use nur\cli\Application2;
use nur\sery\A;
use nur\sery\app\LockFile;
use nur\sery\app\RunFile;
use nur\sery\cl;
use nur\sery\ExitError;
use nur\sery\os\path;
use nur\sery\os\sh;
use nur\sery\output\msg;
use nur\sery\php\func;
use nur\sery\str;
use nur\sery\ValueException;
use nur\sery\wip\app\cli\Application;
#XXX une réécriture de app, qui remplacera app à terme
class app2 {
static ?func $bgapplication_enabled = null;
@ -50,18 +48,35 @@ class app2 {
}
}
static bool $dispach_signals = false;
static function install_signal_handler(bool $allow=true): void {
if (!$allow) return;
$signalHandler = function(int $signo, $siginfo) {
throw new ExitError(255);
};
pcntl_signal(SIGHUP, $signalHandler);
pcntl_signal(SIGINT, $signalHandler);
pcntl_signal(SIGQUIT, $signalHandler);
pcntl_signal(SIGTERM, $signalHandler);
self::$dispach_signals = true;
}
static function _dispatch_signals() {
if (self::$dispach_signals) pcntl_signal_dispatch();
}
#############################################################################
private static function isa_Application($app): bool {
if (!is_string($app)) return false;
return $app === Application::class || is_subclass_of($app, Application::class) ||
$app === Application2::class || is_subclass_of($app, Application2::class);
return $app === Application::class || is_subclass_of($app, Application::class);
}
private static function get_params($app): array {
if ($app instanceof self) {
$params = $app->getParams();
} elseif ($app instanceof Application || $app instanceof Application2) {
} elseif ($app instanceof Application) {
$class = get_class($app);
$params = [
"class" => $class,
@ -107,6 +122,7 @@ class app2 {
*/
static function with($app, $proj=null): self {
$params = self::get_params($app);
$proj ??= self::params_getenv();
$proj ??= self::$app;
$proj_params = $proj !== null? self::get_params($proj): null;
if ($proj_params !== null) {
@ -130,7 +146,26 @@ class app2 {
}
static function get(): self {
return self::$app ??= new self(null);
return self::$app ??= new static(null);
}
static function params_putenv(): void {
$params = serialize(self::get()->getParams());
putenv("NULIB_APP_app_params=". $params);
}
static function params_getenv(): ?array {
$params = getenv("NULIB_APP_app_params");
if ($params === false) return null;
return unserialize($params);
}
static function get_profile(): string {
return self::get()->getProfile();
}
static function set_profile(?string $profile=null): void {
self::get()->setProfile($profile);
}
/**
@ -237,6 +272,7 @@ class app2 {
# my\package\MyApplication --> my-application
$name = preg_replace('/.*\\\\/', "", $name);
$name = str::camel2us($name, false, "-");
$name = str::without_suffix("-app", $name);
}
$this->appgroup = $appgroup;
$this->name = $name;
@ -305,6 +341,11 @@ class app2 {
return $this->profile;
}
function setProfile(?string $profile): void {
$profile ??= $this->profile;
$this->profile = $profile;
}
/**
* @param ?string|false $profile
*
@ -344,8 +385,8 @@ class app2 {
return $this->withProfile(path::join($dirs[0], $names[0]), $profile);
}
function fencedJoin(string $basedir, string $path): string {
$path = path::reljoin($basedir, $path);
function fencedJoin(string $basedir, ?string ...$paths): string {
$path = path::reljoin($basedir, ...$paths);
if (!path::is_within($path, $basedir)) {
throw ValueException::invalid_value($path, "path");
}
@ -355,7 +396,7 @@ class app2 {
#############################################################################
# Paramètres spécifiques à cette application
protected string $appgroup;
protected ?string $appgroup;
function getAppgroup(): ?string {
return $this->appgroup;
@ -394,21 +435,42 @@ class app2 {
];
}
/**
* obtenir le chemin vers le fichier de configuration. par défaut, retourner
* une valeur de la forme "$ETCDIR/$name[.$profile].conf"
*/
function getEtcfile(?string $name=null, $profile=null): string {
if ($name === null) $name = "{$this->name}.conf";
return $this->findFile([$this->etcdir], [$name], $profile);
}
/**
* obtenir le chemin vers le fichier de travail. par défaut, retourner une
* valeur de la forme "$VARDIR/$appgroup/$name[.$profile].tmp"
*/
function getVarfile(?string $name=null, $profile=null): string {
if ($name === null) $name = "{$this->name}.tmp";
$file = $this->withProfile($this->fencedJoin($this->vardir, $name), $profile);
$file = $this->fencedJoin($this->vardir, $this->appgroup, $name);
$file = $this->withProfile($file, $profile);
sh::mkdirof($file);
return $file;
}
/**
* obtenir le chemin vers le fichier de log. par défaut, retourner une
* valeur de la forme "$LOGDIR/$appgroup/$name.log" (sans le profil, parce
* qu'il s'agit du fichier de log par défaut)
*
* Si $name est spécifié, la valeur retournée sera de la forme
* "$LOGDIR/$appgroup/$basename[.$profile].$ext"
*/
function getLogfile(?string $name=null, $profile=null): string {
if ($name === null) $name = "{$this->name}.log";
$file = $this->withProfile($this->fencedJoin($this->logdir, $name), $profile);
if ($name === null) {
$name = "{$this->name}.log";
$profile ??= false;
}
$file = $this->fencedJoin($this->logdir, $this->appgroup, $name);
$file = $this->withProfile($file, $profile);
sh::mkdirof($file);
return $file;
}
@ -416,20 +478,16 @@ class app2 {
/**
* obtenir le chemin absolu vers un fichier de travail
* - si le chemin est absolu, il est inchangé
* - si le chemin est qualifié (commence par ./ ou ../) ou sans chemin, il est
* exprimé par rapport à $vardir
* - sinon le chemin est exprimé par rapport au répertoire de travail de base
* $datadir
* - sinon le chemin est exprimé par rapport à $vardir/$appgroup
*
* is $ensure_dir, créer le répertoire du fichier s'il n'existe pas déjà
* is $ensureDir, créer le répertoire du fichier s'il n'existe pas déjà
*
* la différence est avec {@link self::getVarfile()} est que le fichier peut
* au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de
* valeur par défaut pour $file
*/
function getWorkfile(?string $file, $profile=null, bool $ensureDir=true): ?string {
if ($file === null) return null;
if (path::is_qualified($file) || !path::have_dir($file)) {
$file = path::reljoin($this->vardir, $file);
} else {
$file = path::reljoin($this->datadir, $file);
}
function getWorkfile(string $file, $profile=null, bool $ensureDir=true): string {
$file = path::reljoin($this->vardir, $this->appgroup, $file);
$file = $this->withProfile($file, $profile);
if ($ensureDir) sh::mkdirof($file);
return $file;
@ -439,14 +497,17 @@ class app2 {
* obtenir le chemin absolu vers un fichier spécifié par l'utilisateur.
* - si le chemin commence par /, il est laissé en l'état
* - si le chemin commence par ./ ou ../, il est exprimé par rapport à $cwd
* - sinon le chemin est exprimé par rapport au répertoire de travail $vardir
* - sinon le chemin est exprimé par rapport à $vardir/$appgroup
*
* la différence est avec {@link self::getVarfile()} est que le fichier peut
* au final être situé ailleurs que dans $vardir. de plus, il n'y a pas de
* valeur par défaut pour $file
*/
function getUserfile(?string $file): ?string {
if ($file === null) return null;
function getUserfile(string $file): string {
if (path::is_qualified($file)) {
return path::reljoin($this->cwd, $file);
} else {
return path::reljoin($this->vardir, $file);
return path::reljoin($this->vardir, $this->appgroup, $file);
}
}
@ -455,7 +516,8 @@ class app2 {
function getRunfile(): RunFile {
$name = $this->name;
$runfile = $this->getWorkfile($name);
$logfile = $this->getLogfile($name);
# ne pas spécifier $name pour avoir le fichier par défaut sans profil
$logfile = $this->getLogfile();
return $this->runfile ??= new RunFile($name, $runfile, $logfile);
}

View File

@ -1,45 +1,52 @@
<?php
namespace nur\cli;
namespace nur\sery\wip\app\cli;
use Exception;
use nur\b\ExitError;
use nur\b\ValueException;
use nur\cli\ArgsException;
use nur\cli\ArgsParser;
use nur\config;
use nur\config\ArrayConfig;
use nur\msg;
use nur\os;
use nur\path;
use nur\sery\app\launcher;
use nur\sery\app\RunFile;
use nur\sery\cl;
use nur\sery\output\console as nconsole;
use nur\sery\output\log as nlog;
use nur\sery\output\msg as nmsg;
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\sery\wip\app\app2;
use nur\sery\wip\app\RunFile;
/**
* Class Application: application de base
*/
abstract class Application2 {
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_DATADIR si le projet a pour
* code xxx
* 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.
@ -74,73 +81,39 @@ abstract class Application2 {
/** @var bool faut-il installer le gestionnaire de signaux? */
const USE_SIGNAL_HANDLER = false;
protected static function _app_init(): void {
config::set_fact(config::FACT_CLI_APP);
# avant que l'application soit configurée, configurer le mode debug
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);
# 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);
static function _manage_runfile(RunFile $runfile): void {
global $argc, $argv;
if ($argc <= 1 || $argv[1] !== "//") return;
array_splice($argv, 1, 1); $argc--;
$ec = 0;
switch ($argv[1] ?? "infos") {
case "release-lock":
case "release":
case "rl":
$runfile->release();
break;
case "infos":
case "i":
if ($runfile->isRunning()) {
$action = $runfile->getActionDesc();
if ($action !== null) $action = ": $action";
echo "running$action\n";
} else {
echo "not running\n";
$ec = 1;
}
# 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;
break;
default:
fwrite(STDERR, "$argv[2]: unexpected command\n");
$ec = 123;
}
exit($ec);
}
app2::init(static::class);
nmsg::set_messenger(new StdMessenger([
"min_level" => nmsg::DEBUG,
]));
}
protected static function _app_configure(Application2 $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);
$msgs = ["console" => new StdMessenger([
"min_level" => nmsg::NORMAL,
])];
if (static::USE_LOGFILE) {
$msgs["log"] = new StdMessenger([
"output" => app2::get()->getLogfile(),
"min_level" => nmsg::MINOR,
"add_date" => true,
]);
}
nmsg::init($msgs);
$app->parseArgs();
config::configure();
}
protected static function _app_main(Application2 $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(?Application2 $app=null): void {
static function run(?Application $app=null): void {
$unlock = false;
$stop = false;
$shutdownHandler = function () use (&$unlock, &$stop) {
register_shutdown_function(function() use (&$unlock, &$stop) {
if ($unlock) {
app2::get()->getRunfile()->release();
$unlock = false;
@ -149,30 +122,18 @@ abstract class Application2 {
app2::get()->getRunfile()->wfStop();
$stop = false;
}
};
register_shutdown_function($shutdownHandler);
if (static::USE_SIGNAL_HANDLER) {
$signalHandler = function(int $signo, $siginfo) {
self::exit(255);
};
pcntl_signal(SIGHUP, $signalHandler);
pcntl_signal(SIGINT, $signalHandler);
pcntl_signal(SIGQUIT, $signalHandler);
pcntl_signal(SIGTERM, $signalHandler);
}
});
app2::install_signal_handler(static::USE_SIGNAL_HANDLER);
try {
static::_app_init();
if (static::USE_RUNFILE) {
$runfile = app2::get()->getRunfile();
global $argc, $argv;
if ($argc === 2 && ($argv[1] === "--Application-release-runlock" || $argv[1] === "--ARL")) {
$runfile->release();
exit(0);
}
static::_initialize_app();
$useRunfile = static::USE_RUNFILE;
$useRunlock = static::USE_RUNLOCK;
if ($useRunlock && $runfile->warnIfLocked()) {
exit(1);
}
if ($useRunfile) {
$runfile = app2::get()->getRunfile();
self::_manage_runfile($runfile);
if ($useRunlock && $runfile->warnIfLocked()) exit(1);
$runfile->wfStart();
$stop = true;
if ($useRunlock) {
@ -181,8 +142,8 @@ abstract class Application2 {
}
}
if ($app === null) $app = new static();
static::_app_configure($app);
static::_app_main($app);
static::_configure_app($app);
static::_start_app($app);
} catch (ExitError $e) {
if ($e->haveMessage()) msg::error($e);
exit($e->getCode());
@ -192,6 +153,40 @@ abstract class Application2 {
}
}
protected static function _initialize_app(): void {
app2::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" => app2::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)
@ -217,32 +212,27 @@ abstract class Application2 {
["group",
["-p", "--profile", "--app-profile",
"args" => 1, "argsdesc" => "PROFILE",
"action" => [null, "set_application_profile"],
"action" => [app2::class, "set_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]],
["-P", "--prod", "action" => [app2::class, "set_profile", config::PROD]],
["-T", "--test", "action" => [app2::class, "set_profile", config::TEST]],
["--devel", "action" => [app2::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",
"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"]],
["--sql-trace", "action" => [null, "set_application_sql_trace"]],
],
["-L", "--logfile",
"args" => "file", "argsdesc" => "OUTPUT",
@ -259,75 +249,37 @@ abstract class Application2 {
];
static function set_application_verbosity(string $verbosity): void {
$msg = msg::get();
$nconsole = nconsole::get();
$console = console::get();
switch ($verbosity) {
case "s":
case "silent":
$msg->setLevels([
msg::USER => msg::NEVER,
msg::TECH => msg::NEVER,
msg::EXCEPTION => msg::NEVER,
]);
$nconsole->resetParams([
"min_level" => nmsg::NONE,
]);
break;
case "Q":
case "very-quiet":
$msg->setLevels([
msg::USER => msg::CRITICAL,
msg::TECH => msg::CRITICAL,
msg::EXCEPTION => msg::NEVER,
]);
$nconsole->resetParams([
"min_level" => nmsg::MAJOR,
case "silent":
$console->resetParams([
"min_level" => msg::NONE,
]);
break;
case "q":
case "quiet":
$msg->setLevels([
msg::USER => msg::MAJOR,
msg::TECH => msg::MAJOR,
msg::EXCEPTION => msg::NEVER,
$console->resetParams([
"min_level" => msg::MAJOR,
]);
$nconsole->resetParams([
"min_level" => nmsg::MAJOR,
break;
case "n":
case "normal":
$console->resetParams([
"min_level" => msg::NORMAL,
]);
break;
case "v":
case "verbose":
$msg->setLevels([
msg::USER => msg::MINOR,
msg::TECH => msg::MINOR,
msg::EXCEPTION => msg::NEVER,
]);
$nconsole->resetParams([
"min_level" => nmsg::MINOR,
$console->resetParams([
"min_level" => msg::MINOR,
]);
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);
$nconsole->resetParams([
"min_level" => nmsg::DEBUG,
]);
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);
$nconsole->resetParams([
"min_level" => nmsg::DEBUG,
$console->resetParams([
"min_level" => msg::DEBUG,
]);
break;
default:
@ -335,22 +287,16 @@ abstract class Application2 {
}
}
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]);
nlog::create_or_reset_params([
log::create_or_reset_params([
"output" => $logfile,
], StdMessenger::class, [
"add_date" => true,
"min_level" => nlog::MINOR,
"min_level" => log::MINOR,
]);
}
static function set_application_color(bool $color): void {
msg::get()->setParametrableParams(["color" => $color]);
nconsole::reset_params([
console::reset_params([
"color" => $color,
]);
}
@ -375,8 +321,8 @@ abstract class Application2 {
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();
static function get_profile(?string $profile=null): string {
if ($profile === null) $profile = app2::get_profile();
foreach (static::PROFILE_COLORS as $text => $color) {
if (strpos($profile, $text) !== false) {
return $color? "<color $color>$profile</color>": $profile;
@ -387,15 +333,4 @@ abstract class Application2 {
}
abstract function main();
const BGLAUNCH_SCRIPT = null;
static function runfile(): RunFile {
$callerParams = app2::get()->getParams();
return app2::with(static::class, $callerParams)->getRunfile();
}
static function bglaunch(?array $args=null) {
launcher::launch(static::class, cl::merge([
static::BGLAUNCH_SCRIPT,
], $args));
}
}

View File

@ -2,7 +2,7 @@
* [ ] implémenter les arguments avancés avec le préfixe "++" sur la description
* [ ] pour le nombre d'arguments, supporter l'alias `*` pour `0..N` et `+` pour `1..N`
* [ ] transformer un schéma en définition d'arguments, un tableau en liste d'arguments, et vice-versa
actuellement, même si la surcharge d'option fonctionne, l'affichage de l'aide est incorrecte
* possibilité de merger *une définition d'option*

View File

@ -1,6 +1,7 @@
<?php
namespace nur\sery\app;
namespace nur\sery\wip\app;
use nur\sery\app\Runfile;
use nur\sery\cl;
use nur\sery\file\TmpfileWriter;
use nur\sery\os\path;
@ -10,7 +11,7 @@ use nur\sery\StateException;
use nur\sery\str;
use nur\sery\wip\app\app2;
class launcher2 {
class launcher {
/**
* transformer une liste d'argument de la forme
* - ["myArg" => $value] devient ["--my-arg", "$value"]

View File

@ -1,72 +0,0 @@
<?php
namespace nur\sery\wip\cli;
use Exception;
use nur\sery\ExitError;
use nur\sery\output\msg;
use nur\sery\output\std\StdMessenger;
/**
* Class Application: une application en ligne de commande
*/
abstract class Application {
protected static function _app_init(): void {
msg::set_messenger_class(StdMessenger::class);
}
protected static function _app_configure(Application $app): void {
$app->parseArgs();
}
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 {
try {
static::_app_init();
if ($app === null) $app = new static();
static::_app_configure($app);
static::_app_main($app);
} catch (ExitError $e) {
msg::error($e->getUserMessage());
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)
*
* é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 ARGS = [];
/** @throws ArgsException */
function parseArgs(array $args=null): void {
$parser = new ArgsParser(static::ARGS);
$parser->parse($this, $args);
}
abstract function main();
}

View File

@ -1,11 +0,0 @@
<?php
namespace nur\sery\wip\cli;
use nur\sery\UserException;
/**
* Class ArgsException: exception lancée quand il y a une erreur dans l'analyse
* des arguments de la ligne de commande
*/
class ArgsException extends UserException {
}

File diff suppressed because it is too large Load Diff

View File

@ -1,83 +0,0 @@
<?php
namespace nur\sery\wip\cli;
use nur\A;
use nur\sery\php\nur_func;
/**
* Class DynamicCommand: implémentation par défaut de {@link IDynamicCommand}
*
*/
class DynamicCommand implements IDynamicCommand {
/**
* retourner la liste des commandes sous la forme d'un tableau associatif avec
* des éléments { $command => $cdef }
*/
protected function COMMANDS(): array {
return static::COMMANDS;
} const COMMANDS = null;
private $commands;
private $dcommands;
private $aliases;
protected function buildCommands(): void {
if ($this->commands !== null) return;
$commands = [];
$dcommands = [];
$aliases = [];
$index = 0;
foreach ($this->COMMANDS() as $key => $cdef) {
if ($key === $index) {
$index++;
[$cnames, $assoc] = A::split_assoc($cdef);
$cname = $cnames[0];
if ($cname === null) {
# commande complètement dynamique
$dcommands[] = $cnames[2];
if ($cnames[1] === null) continue;
$cdef = [null, $cnames[1]];
$cname = $cnames[1][0];
$cnames = [];
}
} else {
$cname = $key;
$cnames = [$cname];
[$seq, $assoc] = A::split_assoc($cdef);
A::merge($cnames, $seq);
A::merge_assoc($cdef, $cnames, $assoc, true);
}
$commands[$cname] = $cdef;
foreach ($cnames as $key) {
$aliases[$key] = $cname;
}
}
$this->commands = $commands;
$this->dcommands = $dcommands;
$this->aliases = $aliases;
}
function getCommands(): ?array {
$this->buildCommands();
return array_keys($this->commands);
}
function getCommandDefs(string $command, bool $virtual): ?array {
$this->buildCommands();
$command = A::get($this->aliases, $command, $command);
$cdef = A::get($this->commands, $command);
if ($cdef !== null) {
if ($cdef[0] === null) {
if ($virtual) $cdef = $cdef[1];
else return null;
}
return $cdef !== null? [$cdef]: null;
}
# tester les commandes complètement dynamiques
foreach ($this->dcommands as $func) {
$cdef = nur_func::call($func, $command);
if ($cdef !== null) return [$cdef];
}
return null;
}
}

View File

@ -1,31 +0,0 @@
<?php
namespace nur\sery\wip\cli;
use nur\sery\php\nur_func;
class DynamicCommandMethod implements IDynamicCommand {
function __construct($func) {
$this->func = $func;
}
/** @var object */
private $dest;
function setDest($dest): void {
if (!is_object($dest)) $dest = null;
$this->dest = $dest;
}
function getCommands(): ?array {
return null;
}
private $func;
function getCommandDefs(string $command, bool $virtual): ?array {
$func = $this->func;
$func_args = [$command];
nur_func::check_func($func, $this->dest, $func_args);
return nur_func::call($func, ...$func_args);
}
}

View File

@ -1,19 +0,0 @@
<?php
namespace nur\sery\wip\cli;
/**
* Class IDynamicCommand: gestionnaire de commandes dynamiques
*/
interface IDynamicCommand {
/**
* retourner la liste des commandes valides, ou null si cette liste ne peut
* pas être construite
*/
function getCommands(): ?array;
/**
* retourner les définitions pour la commande spécifiée, ou null si elle n'est
* pas valide
*/
function getCommandDefs(string $command, bool $virtual): ?array;
}

View File

@ -0,0 +1,28 @@
<?php
namespace nur\sery\wip\tools;
use nur\sery\output\msg;
use nur\sery\wip\app\app2;
use nur\sery\wip\app\cli\Application;
class SteamTrainApp extends Application {
const TITLE = "Train à vapeur";
const USE_SIGNAL_HANDLER = true;
const USE_LOGFILE = true;
const USE_RUNFILE = true;
const USE_RUNLOCK = true;
const ARGS = [
"purpose" => self::TITLE,
];
function main() {
$runfile = app2::get()->getRunfile();
$runfile->action("Running train...", 100);
for ($i = 1; $i <= 100; $i++) {
msg::print("Tchou-tchou! x $i");
$runfile->step();
sleep(1);
}
}
}