diff --git a/.gitignore b/.gitignore
index 28b881a..64d3f03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,8 @@
-/.phpunit.result.cache
+/devel/*
+!/devel/.keep
.~lock*#
.*.swp
/vendor/
+
+/.phpunit.result.cache
diff --git a/devel/.keep b/devel/.keep
new file mode 100644
index 0000000..e69de29
diff --git a/nur_bin/steam-train.php b/nur_bin/steam-train.php
new file mode 100755
index 0000000..09f855c
--- /dev/null
+++ b/nur_bin/steam-train.php
@@ -0,0 +1,7 @@
+#!/usr/bin/php
+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);
+ 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;
+ }
+
+ 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 {
+ $unlock = false;
+ $stop = false;
+ $shutdownHandler = function () use (&$unlock, &$stop) {
+ if ($unlock) {
+ app2::get()->getRunfile()->release();
+ $unlock = false;
+ }
+ if ($stop) {
+ 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);
+ }
+ 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);
+ }
+ $useRunlock = static::USE_RUNLOCK;
+ if ($useRunlock && $runfile->warnIfLocked()) {
+ exit(1);
+ }
+ $runfile->wfStart();
+ $stop = true;
+ if ($useRunlock) {
+ $runfile->lock();
+ $unlock = true;
+ }
+ }
+ if ($app === null) $app = new static();
+ static::_app_configure($app);
+ static::_app_main($app);
+ } catch (ExitError $e) {
+ if ($e->haveMessage()) msg::error($e);
+ 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 PROFILE_SECTION = [
+ "title" => "PROFILS D'EXECUTION",
+ ["group",
+ ["-p", "--profile", "--app-profile",
+ "args" => 1, "argsdesc" => "PROFILE",
+ "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",
+ "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",
+ "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 {
+ $msg = msg::get();
+ $nconsole = nconsole::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,
+ ]);
+ break;
+ case "q":
+ case "quiet":
+ $msg->setLevels([
+ msg::USER => msg::MAJOR,
+ msg::TECH => msg::MAJOR,
+ msg::EXCEPTION => msg::NEVER,
+ ]);
+ $nconsole->resetParams([
+ "min_level" => nmsg::MAJOR,
+ ]);
+ 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,
+ ]);
+ 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,
+ ]);
+ 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]);
+ nlog::create_or_reset_params([
+ "output" => $logfile,
+ ], StdMessenger::class, [
+ "add_date" => true,
+ "min_level" => nlog::MINOR,
+ ]);
+ }
+ static function set_application_color(bool $color): void {
+ msg::get()->setParametrableParams(["color" => $color]);
+ nconsole::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 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? "$profile": $profile;
+ }
+ }
+ $color = static::DEFAULT_PROFILE_COLOR;
+ return $color? "$profile": $profile;
+ }
+
+ 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));
+ }
+}
diff --git a/nur_src/cli/BgApplication2.php b/nur_src/cli/BgApplication2.php
new file mode 100644
index 0000000..13e844d
--- /dev/null
+++ b/nur_src/cli/BgApplication2.php
@@ -0,0 +1,24 @@
+ 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);
+ }
+}
diff --git a/nur_src/tools/SteamTrainApp.php b/nur_src/tools/SteamTrainApp.php
new file mode 100644
index 0000000..1c00d1d
--- /dev/null
+++ b/nur_src/tools/SteamTrainApp.php
@@ -0,0 +1,23 @@
+ [BgApplication2::ARGS, parent::ARGS],
+ "purpose" => self::TITLE,
+ ];
+
+ function main() {
+ for ($i = 1; $i <= 100; $i++) {
+ msg::print("Tchou-tchou! x $i");
+ sleep(1);
+ }
+ }
+}
diff --git a/src/app/launcher2.php b/src/app/launcher2.php
new file mode 100644
index 0000000..4d21b18
--- /dev/null
+++ b/src/app/launcher2.php
@@ -0,0 +1,136 @@
+ $value] devient ["--my-arg", "$value"]
+ * - ["myOpt" => true] devient ["--my-opt"]
+ * - ["myOpt" => false] est momis
+ * - les valeurs séquentielles sont prises telles quelles
+ */
+ static function verifix_args(array $args): array {
+ if (!cl::is_list($args)) {
+ $fixedArgs = [];
+ $index = 0;
+ foreach ($args as $arg => $value) {
+ if ($arg === $index) {
+ $index++;
+ $fixedArgs[] = $value;
+ continue;
+ } elseif ($value === false) {
+ continue;
+ }
+ $arg = str::us2camel($arg);
+ $arg = str::camel2us($arg, false, "-");
+ $arg = str_replace("_", "-", $arg);
+ $fixedArgs[] = "--$arg";
+ if ($value !== true) $fixedArgs[] = "$value";
+ }
+ $args = $fixedArgs;
+ }
+ # corriger le chemin de l'application pour qu'il soit absolu et normalisé
+ $args[0] = path::abspath($args[0]);
+ return $args;
+ }
+
+ static function launch(string $appClass, array $args): int {
+ $app = app2::get();
+ $vendorBindir = $app->getVendorbindir();
+ $launch_php = "$vendorBindir/_launch.php";
+ if (!file_exists($launch_php)) {
+ $launch_php = __DIR__."/../../lib/_launch.php";
+ }
+ $tmpfile = new TmpfileWriter();
+ $tmpfile->keep()->serialize($app->getParams());
+
+ $args = self::verifix_args($args);
+ $cmd = new Cmd([
+ $launch_php,
+ "--internal-use", $tmpfile->getFile(),
+ $appClass, "--", ...$args,
+ ]);
+ $cmd->addRedir("both", "/tmp/nulib_app_launcher-launch.log");
+ $cmd->passthru($exitcode);
+
+ # attendre un peu que la commande aie le temps de s'initialiser
+ sleep(1);
+
+ $tmpfile->close();
+ return $exitcode;
+ }
+
+ static function _start(array $args, Runfile $runfile): bool {
+ if ($runfile->warnIfLocked()) return false;
+ $pid = pcntl_fork();
+ if ($pid == -1) {
+ # parent, impossible de forker
+ throw new StateException("unable to fork");
+ } elseif ($pid) {
+ # parent, fork ok
+ return true;
+ } else {
+ ## child, fork ok
+ # Créer un groupe de process, pour pouvoir tuer tous les enfants en même temps
+ $runfile->tm_startPg();
+ $logfile = $runfile->getLogfile() ?? "/tmp/nulib_app_launcher-_start.log";
+ $pid = posix_getpid();
+ $exitcode = -776;
+ try {
+ # puis lancer la commande
+ $cmd = new Cmd($args);
+ $cmd->addSource("/g/init.env");
+ $cmd->addRedir("both", $logfile, true);
+ msg::debug("$pid: launching\n".$cmd->getCmd());
+ $cmd->fork_exec($exitcode);
+ msg::debug("$pid: exitcode=$exitcode");
+ return true;
+ } finally {
+ $runfile->wfStopped($exitcode);
+ }
+ }
+ }
+
+ static function _stop(Runfile $runfile): void {
+ $data = $runfile->read();
+ $pid = $data["pg_pid"];
+ if ($pid === null) {
+ msg::warning("$data[name]: groupe de process inconnu");
+ return;
+ }
+ msg::action("kill $pid");
+ if (!posix_kill(-$pid, SIGKILL)) {
+ switch (posix_get_last_error()) {
+ case PCNTL_ESRCH:
+ msg::afailure("process inexistant");
+ break;
+ case PCNTL_EPERM:
+ msg::afailure("process non accessible");
+ break;
+ case PCNTL_EINVAL:
+ msg::afailure("signal invalide");
+ break;
+ }
+ return;
+ }
+ $timeout = 10;
+ while ($runfile->tm_isUndead($pid)) {
+ sleep(1);
+ if (--$timeout == 0) {
+ msg::afailure("impossible d'arrêter la tâche");
+ return;
+ }
+ }
+ $runfile->wfStopped(-778);
+ msg::asuccess();
+ }
+}
diff --git a/tests/wip/app/appTest.php b/tests/wip/app/appTest.php
new file mode 100644
index 0000000..dca7ab3
--- /dev/null
+++ b/tests/wip/app/appTest.php
@@ -0,0 +1,17 @@
+call($app, "$name"));
+ }
+}
diff --git a/wip/app/app.php b/wip/app/app.php
index 1377ab2..a403bb8 100644
--- a/wip/app/app.php
+++ b/wip/app/app.php
@@ -27,6 +27,7 @@ class app {
private static function verifix_name(string &$name): void {
# si $name est une classe, enlever le package et normaliser
+ # my\package\MyApplication --> my-application
$name = preg_replace('/.*\\\\/', "", $name);
$name = str::without_suffix("-app", str::camel2us($name, false, "-"));
}
diff --git a/wip/app/app2.php b/wip/app/app2.php
new file mode 100644
index 0000000..76e940c
--- /dev/null
+++ b/wip/app/app2.php
@@ -0,0 +1,419 @@
+invoke());
+ if (!$forceEnabled && !$enabled) {
+ msg::debug("Planifications désactivées. L'application n'a pas été lancée");
+ throw new ExitError();
+ }
+ }
+
+ #############################################################################
+
+ 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);
+ }
+
+ private static function get_params($app): array {
+ if ($app instanceof self) {
+ $params = $app->getParams();
+ } elseif ($app instanceof Application || $app instanceof Application2) {
+ $params = [
+ "projdir" => $app::PROJDIR,
+ "vendor" => $app::VENDOR,
+ "appcode" => $app::APPCODE,
+ "datadir" => $app::DATADIR,
+ "etcdir" => $app::ETCDIR,
+ "vardir" => $app::VARDIR,
+ "logdir" => $app::LOGDIR,
+ "apptype" => "cli",
+ "name" => $app::NAME,
+ "title" => $app::TITLE,
+ ];
+ } elseif (self::isa_Application($app)) {
+ $params = [
+ "projdir" => constant("$app::PROJDIR"),
+ "vendor" => constant("$app::VENDOR"),
+ "appcode" => constant("$app::APPCODE"),
+ "datadir" => constant("$app::DATADIR"),
+ "etcdir" => constant("$app::ETCDIR"),
+ "vardir" => constant("$app::VARDIR"),
+ "logdir" => constant("$app::LOGDIR"),
+ "apptype" => "cli",
+ "name" => constant("$app::NAME"),
+ "title" => constant("$app::TITLE"),
+ ];
+ } elseif (is_array($app)) {
+ $params = $app;
+ } else {
+ throw ValueException::invalid_type($app, Application::class);
+ }
+ return $params;
+ }
+
+ /**
+ * @param Application|string $app
+ * @param Application|string $proj
+ */
+ static function with($app, $proj=null): self {
+ $params = self::get_params($app);
+ $proj_params = $proj !== null? self::get_params($proj): null;
+ if ($proj_params !== null) {
+ A::merge($params, cl::select($proj_params, [
+ "projdir",
+ "vendor",
+ "appcode",
+ "datadir",
+ "etcdir",
+ "vardir",
+ "logdir",
+ ]));
+ }
+ return new static($params, $proj_params !== null);
+ }
+
+ protected static ?self $app = null;
+
+ static function init($app, $proj=null): void {
+ self::$app = static::with($app, $proj);
+ }
+
+ static function get(): self {
+ return self::$app ??= new self(null);
+ }
+
+ /**
+ * @var array répertoires vendor exprimés relativement à PROJDIR
+ */
+ const DEFAULT_VENDOR = [
+ "bindir" => "vendor/bin",
+ "autoload" => "vendor/autoload.php",
+ ];
+
+ function __construct(?array $params, bool $useProjParams=false) {
+ if ($useProjParams) {
+ [
+ "projdir" => $projdir,
+ "vendor" => $vendor,
+ "appcode" => $appcode,
+ "cwd" => $cwd,
+ "datadir" => $datadir,
+ "etcdir" => $etcdir,
+ "vardir" => $vardir,
+ "logdir" => $logdir,
+ "profile" => $profile,
+ ] = $params;
+ } else {
+ $projdir = path::abspath($params["projdir"] ?? ".");
+ $vendor = $params["vendor"] ?? self::DEFAULT_VENDOR;
+ $vendor["bindir"] = path::reljoin($projdir, $vendor["bindir"]);
+ $vendor["autoload"] = path::reljoin($projdir, $vendor["autoload"]);
+ $appcode = $params["appcode"] ?? "app";
+ $APPCODE = str_replace("-", "_", strtoupper($appcode));
+ # cwd
+ $cwd = getcwd();
+ # datadir
+ $datadir = getenv("${APPCODE}_DATADIR");
+ $datadirIsDefined = $datadir !== false;
+ if ($datadir === false) $datadir = $params["datadir"] ?? null;
+ if ($datadir === null) $datadir = "devel/data";
+ $datadir = path::reljoin($projdir, $datadir);
+ # etcdir
+ $etcdir = getenv("${APPCODE}_ETCDIR");
+ if ($etcdir === false) $etcdir = $params["etcdir"] ?? null;
+ if ($etcdir === null) $etcdir = "etc";
+ $etcdir = path::reljoin($datadir, $etcdir);
+ # vardir
+ $vardir = getenv("${APPCODE}_VARDIR");
+ if ($vardir === false) $vardir = $params["vardir"] ?? null;
+ if ($vardir === null) $vardir = "var";
+ $vardir = path::reljoin($datadir, $vardir);
+ # logdir
+ $logdir = getenv("${APPCODE}_LOGDIR");
+ if ($logdir === false) $logdir = $params["logdir"] ?? null;
+ if ($logdir === null) $logdir = "log";
+ $logdir = path::reljoin($datadir, $logdir);
+ # profile
+ $profile = getenv("${APPCODE}_PROFILE");
+ if ($profile === false) $profile = getenv("APP_PROFILE");
+ if ($profile === false) $profile = $params["profile"] ?? null;
+ if ($profile === null) $profile = $datadirIsDefined? "prod": "devel";
+ }
+ $this->projdir = $projdir;
+ $this->vendor = $vendor;
+ $this->appcode = $appcode;
+ $this->cwd = $cwd;
+ $this->datadir = $datadir;
+ $this->etcdir = $etcdir;
+ $this->vardir = $vardir;
+ $this->logdir = $logdir;
+ $this->profile = $profile;
+
+ # apptype, name, title
+ $this->apptype = $params["apptype"] ?? "cli";
+ $name = $params["name"] ?? null;
+ if ($name === null) {
+ $name = $appcode;
+ } else {
+ # si $name est une classe, enlever le package et normaliser i.e
+ # my\package\MyApplication --> my-application
+ $name = preg_replace('/.*\\\\/', "", $name);
+ $name = str::without_suffix("-app", str::camel2us($name, false, "-"));
+ }
+ $this->name = $name;
+ $this->title = $params["title"] ?? null;
+ }
+
+ #############################################################################
+ # Paramètres partagés par tous les scripts d'un projet (et les scripts lancés
+ # à partir d'une application de ce projet)
+
+ protected string $projdir;
+
+ function getProjdir(): string {
+ return $this->projdir;
+ }
+
+ protected array $vendor;
+
+ function getVendorBindir(): string {
+ return $this->vendor["bindir"];
+ }
+
+ function getVendorAutoload(): string {
+ return $this->vendor["autoload"];
+ }
+
+ protected string $appcode;
+
+ function getAppcode(): string {
+ return $this->appcode;
+ }
+
+ protected string $cwd;
+
+ function getCwd(): string {
+ return $this->cwd;
+ }
+
+ protected string $datadir;
+
+ function getDatadir(): string {
+ return $this->datadir;
+ }
+
+ protected string $etcdir;
+
+ function getEtcdir(): string {
+ return $this->etcdir;
+ }
+
+ protected string $vardir;
+
+ function getVardir(): string {
+ return $this->vardir;
+ }
+
+ protected string $logdir;
+
+ function getLogdir(): string {
+ return $this->logdir;
+ }
+
+ protected string $profile;
+
+ function getProfile(): string {
+ return $this->profile;
+ }
+
+ /**
+ * @param ?string|false $profile
+ */
+ function withProfile(string $file, $profile): string {
+ if ($profile !== false) {
+ if ($profile === null) $profile = $this->getProfile();
+ [$dir, $filename] = path::split($file);
+ $basename = path::basename($filename);
+ $ext = path::ext($file);
+ $file = path::join($dir, "$basename.$profile$ext");
+ }
+ return $file;
+ }
+
+ function findFile(array $dirs, array $names, $profile=null): string {
+ # d'abord chercher avec le profil
+ if ($profile !== false) {
+ foreach ($dirs as $dir) {
+ foreach ($names as $name) {
+ $file = path::join($dir, $name);
+ $file = $this->withProfile($file, $profile);
+ if (file_exists($file)) return $file;
+ }
+ }
+ }
+ # puis sans profil
+ foreach ($dirs as $dir) {
+ foreach ($names as $name) {
+ $file = path::join($dir, $name);
+ if (file_exists($file)) return $file;
+ }
+ }
+ # la valeur par défaut est avec profil
+ return $this->withProfile(path::join($dirs[0], $names[0]), $profile);
+ }
+
+ function fencedJoin(string $basedir, string $path): string {
+ $path = path::reljoin($basedir, $path);
+ if (!path::is_within($path, $basedir)) {
+ throw ValueException::invalid_value($path, "path");
+ }
+ return $path;
+ }
+
+ #############################################################################
+ # Paramètres spécifiques à cette application
+
+ protected string $apptype;
+
+ function getApptype(): string {
+ return $this->apptype;
+ }
+
+ protected string $name;
+
+ function getName(): ?string {
+ return $this->name;
+ }
+
+ protected ?string $title;
+
+ function getTitle(): ?string {
+ return $this->title;
+ }
+
+ #############################################################################
+ # Méthodes outils
+
+ /** recréer le tableau des paramètres */
+ function getParams(): array {
+ return [
+ "projdir" => $this->projdir,
+ "vendor" => $this->vendor,
+ "appcode" => $this->appcode,
+ "cwd" => $this->cwd,
+ "datadir" => $this->datadir,
+ "etcdir" => $this->etcdir,
+ "vardir" => $this->vardir,
+ "logdir" => $this->logdir,
+ "profile" => $this->profile,
+ "apptype" => $this->apptype,
+ "name" => $this->name,
+ "title" => $this->title,
+ ];
+ }
+
+ function getEtcfile(?string $name=null, $profile=null): string {
+ if ($name === null) $name = "{$this->name}.conf";
+ return $this->findFile([$this->etcdir], [$name], $profile);
+ }
+
+ function getVarfile(?string $name=null, $profile=null): string {
+ if ($name === null) $name = "{$this->name}.tmp";
+ $file = $this->withProfile($this->fencedJoin($this->vardir, $name), $profile);
+ sh::mkdirof($file);
+ return $file;
+ }
+
+ function getLogfile(?string $name=null, $profile=null): string {
+ if ($name === null) $name = "{$this->name}.log";
+ $file = $this->withProfile($this->fencedJoin($this->logdir, $name), $profile);
+ sh::mkdirof($file);
+ return $file;
+ }
+
+ /**
+ * 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
+ *
+ * is $ensure_dir, créer le répertoire du fichier s'il n'existe pas déjà
+ */
+ 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);
+ }
+ $file = $this->withProfile($file, $profile);
+ if ($ensureDir) sh::mkdirof($file);
+ return $file;
+ }
+
+ /**
+ * 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
+ */
+ function getUserfile(?string $file): ?string {
+ if ($file === null) return null;
+ if (path::is_qualified($file)) {
+ return path::reljoin($this->cwd, $file);
+ } else {
+ return path::reljoin($this->vardir, $file);
+ }
+ }
+
+ protected ?RunFile $runfile = null;
+
+ function getRunfile(): RunFile {
+ $name = $this->name;
+ $runfile = $this->getWorkfile($name);
+ $logfile = $this->getLogfile($name);
+ return $this->runfile ??= new RunFile($name, $runfile, $logfile);
+ }
+
+ protected ?array $lockFiles = null;
+
+ function getLockfile(?string $name=null): LockFile {
+ $this->lockFiles[$name] ??= $this->getRunfile()->getLockFile($name, $this->title);
+ return $this->lockFiles[$name];
+ }
+}