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]; + } +}