551 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			551 lines
		
	
	
		
			16 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nur\sery;
 | |
| 
 | |
| use nulib\A;
 | |
| use nulib\app\LockFile;
 | |
| use nulib\app\RunFile;
 | |
| use nulib\cl;
 | |
| use nulib\ExitError;
 | |
| use nulib\os\path;
 | |
| use nulib\os\sh;
 | |
| use nulib\php\func;
 | |
| use nulib\str;
 | |
| use nulib\ValueException;
 | |
| use nur\cli\Application as nur_Application;
 | |
| use nur\sery\app\cli\Application;
 | |
| 
 | |
| class app {
 | |
|   private static function isa_Application($app): bool {
 | |
|     if (!is_string($app)) return false;
 | |
|     return $app === Application::class || is_subclass_of($app, Application::class)
 | |
|       || $app === nur_Application::class || is_subclass_of($app, nur_Application::class);
 | |
|   }
 | |
| 
 | |
|   private static function get_params($app): array {
 | |
|     if ($app instanceof self) {
 | |
|       $params = $app->getParams();
 | |
|     } elseif ($app instanceof Application) {
 | |
|       $class = get_class($app);
 | |
|       $params = [
 | |
|         "class" => $class,
 | |
|         "projdir" => $app::PROJDIR,
 | |
|         "vendor" => $app::VENDOR,
 | |
|         "appcode" => $app::APPCODE,
 | |
|         "datadir" => $app::DATADIR,
 | |
|         "etcdir" => $app::ETCDIR,
 | |
|         "vardir" => $app::VARDIR,
 | |
|         "logdir" => $app::LOGDIR,
 | |
|         "appgroup" => $app::APPGROUP,
 | |
|         "name" => $app::NAME,
 | |
|         "title" => $app::TITLE,
 | |
|       ];
 | |
|     } elseif (self::isa_Application($app)) {
 | |
|       $class = $app;
 | |
|       $params = [
 | |
|         "class" => $class,
 | |
|         "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"),
 | |
|         "appgroup" => constant("$app::APPGROUP"),
 | |
|         "name" => constant("$app::NAME"),
 | |
|         "title" => constant("$app::TITLE"),
 | |
|       ];
 | |
|     } elseif (is_array($app)) {
 | |
|       $params = $app;
 | |
|     } else {
 | |
|       throw ValueException::invalid_type($app, Application::class);
 | |
|     }
 | |
|     return $params;
 | |
|   }
 | |
| 
 | |
|   protected static ?self $app = null;
 | |
| 
 | |
|   /**
 | |
|    * @param Application|string|array $app
 | |
|    * @param Application|string|array|null $proj
 | |
|    */
 | |
|   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) {
 | |
|       A::merge($params, cl::select($proj_params, [
 | |
|         "projdir",
 | |
|         "vendor",
 | |
|         "appcode",
 | |
|         "cwd",
 | |
|         "datadir",
 | |
|         "etcdir",
 | |
|         "vardir",
 | |
|         "logdir",
 | |
|         "profile",
 | |
|       ]));
 | |
|     }
 | |
|     return new static($params, $proj_params !== null);
 | |
|   }
 | |
| 
 | |
|   static function init($app, $proj=null): void {
 | |
|     self::$app = static::with($app, $proj);
 | |
|   }
 | |
| 
 | |
|   static function get(): self {
 | |
|     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);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @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,
 | |
|         "datadir" => $datadir,
 | |
|         "etcdir" => $etcdir,
 | |
|         "vardir" => $vardir,
 | |
|         "logdir" => $logdir,
 | |
|       ] = $params;
 | |
|       $cwd = $params["cwd"] ?? null;
 | |
|       $datadirIsDefined = true;
 | |
|       $profile = $params["profile"] ?? null;
 | |
|     } else {
 | |
|       # projdir
 | |
|       $projdir = $params["projdir"] ?? null;
 | |
|       if ($projdir === null) {
 | |
|         global $_composer_autoload_path, $_composer_bin_dir;
 | |
|         $autoload = $_composer_autoload_path ?? null;
 | |
|         $bindir = $_composer_bin_dir ?? null;
 | |
|         if ($autoload !== null) {
 | |
|           $vendor = preg_replace('/\/[^\/]+\.php$/', "", $autoload);
 | |
|           $bindir ??= "$vendor/bin";
 | |
|           $projdir = preg_replace('/\/[^\/]+$/', "", $vendor);
 | |
|           $params["vendor"] = [
 | |
|             "autoload" => $autoload,
 | |
|             "bindir" => $bindir,
 | |
|           ];
 | |
|         }
 | |
|       }
 | |
|       if ($projdir === null) $projdir = ".";
 | |
|       $projdir = path::abspath($projdir);
 | |
|       # vendor
 | |
|       $vendor = $params["vendor"] ?? self::DEFAULT_VENDOR;
 | |
|       $vendor["bindir"] = path::reljoin($projdir, $vendor["bindir"]);
 | |
|       $vendor["autoload"] = path::reljoin($projdir, $vendor["autoload"]);
 | |
|       # appcode
 | |
|       $appcode = $params["appcode"] ?? null;
 | |
|       if ($appcode === null) {
 | |
|         $appcode = str::without_suffix("-app", path::basename($projdir));
 | |
|       }
 | |
|       $APPCODE = str_replace("-", "_", strtoupper($appcode));
 | |
|       # cwd
 | |
|       $cwd = $params["cwd"] ?? null;
 | |
|       # datadir
 | |
|       $datadir = getenv("${APPCODE}_DATADIR");
 | |
|       $datadirIsDefined = $datadir !== false;
 | |
|       if ($datadir === false) $datadir = $params["datadir"] ?? null;
 | |
|       if ($datadir === null) $datadir = "devel";
 | |
|       $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;
 | |
|     }
 | |
|     # cwd
 | |
|     $cwd ??= getcwd();
 | |
|     # profile
 | |
|     $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;
 | |
| 
 | |
|     # name, title
 | |
|     $appgroup = $params["appgroup"] ?? null;
 | |
|     $name = $params["name"] ?? $params["class"] ?? 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::camel2us($name, false, "-");
 | |
|       $name = str::without_suffix("-app", $name);
 | |
|     }
 | |
|     $this->appgroup = $appgroup;
 | |
|     $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;
 | |
|   }
 | |
| 
 | |
|   function setProfile(?string $profile): void {
 | |
|     $profile ??= $this->profile;
 | |
|     $this->profile = $profile;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * @param ?string|false $profile
 | |
|    *
 | |
|    * false === pas de profil
 | |
|    * null === profil par défaut
 | |
|    */
 | |
|   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 ...$paths): string {
 | |
|     $path = path::reljoin($basedir, ...$paths);
 | |
|     if (!path::is_within($path, $basedir)) {
 | |
|       throw ValueException::invalid_value($path, "path");
 | |
|     }
 | |
|     return $path;
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
|   # Paramètres spécifiques à cette application
 | |
| 
 | |
|   protected ?string $appgroup;
 | |
| 
 | |
|   function getAppgroup(): ?string {
 | |
|     return $this->appgroup;
 | |
|   }
 | |
| 
 | |
|   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,
 | |
|       "appgroup" => $this->appgroup,
 | |
|       "name" => $this->name,
 | |
|       "title" => $this->title,
 | |
|     ];
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * 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->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";
 | |
|       $profile ??= false;
 | |
|     }
 | |
|     $file = $this->fencedJoin($this->logdir, $this->appgroup, $name);
 | |
|     $file = $this->withProfile($file, $profile);
 | |
|     sh::mkdirof($file);
 | |
|     return $file;
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * obtenir le chemin absolu vers un fichier de travail
 | |
|    * - si le chemin est absolu, il est inchangé
 | |
|    * - sinon le chemin est exprimé par rapport à $vardir/$appgroup
 | |
|    *
 | |
|    * 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 {
 | |
|     $file = path::reljoin($this->vardir, $this->appgroup, $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 à $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 (path::is_qualified($file)) {
 | |
|       return path::reljoin($this->cwd, $file);
 | |
|     } else {
 | |
|       return path::reljoin($this->vardir, $this->appgroup, $file);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected ?RunFile $runfile = null;
 | |
| 
 | |
|   function getRunfile(): RunFile {
 | |
|     $name = $this->name;
 | |
|     $runfile = $this->getWorkfile($name);
 | |
|     $logfile = $this->getLogfile("$name.out", false);
 | |
|     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];
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
| 
 | |
|   const EC_FORK_CHILD = 250;
 | |
|   const EC_FORK_PARENT = 251;
 | |
|   const EC_DISABLED = 252;
 | |
|   const EC_LOCKED = 253;
 | |
|   const EC_BAD_COMMAND = 254;
 | |
|   const EC_UNEXPECTED = 255;
 | |
| 
 | |
|   #############################################################################
 | |
| 
 | |
|   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(128 + $signo);
 | |
|     };
 | |
|     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();
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
| 
 | |
|   static ?func $bgapplication_enabled = null;
 | |
| 
 | |
|   /**
 | |
|    * spécifier la fonction permettant de vérifier si l'exécution de tâches
 | |
|    * de fond est autorisée. Si cette méthode n'est pas utilisée, par défaut,
 | |
|    * les tâches planifiées sont autorisées
 | |
|    *
 | |
|    * si $func===true, spécifier une fonction qui retourne toujours vrai
 | |
|    * si $func===false, spécifiée une fonction qui retourne toujours faux
 | |
|    * sinon, $func doit être une fonction valide
 | |
|    */
 | |
|   static function set_bgapplication_enabled($func): void {
 | |
|     if (is_bool($func)) {
 | |
|       $enabled = $func;
 | |
|       $func = function () use ($enabled) {
 | |
|         return $enabled;
 | |
|       };
 | |
|     }
 | |
|     self::$bgapplication_enabled = func::with($func);
 | |
|   }
 | |
| 
 | |
|   /**
 | |
|    * Si les exécutions en tâche de fond sont autorisée, retourner. Sinon
 | |
|    * afficher une erreur et quitter l'application
 | |
|    */
 | |
|   static function check_bgapplication_enabled(bool $forceEnabled=false): void {
 | |
|     if (self::$bgapplication_enabled === null || $forceEnabled) return;
 | |
|     if (!self::$bgapplication_enabled->invoke()) {
 | |
|       throw new ExitError(self::EC_DISABLED, "Planifications désactivées. La tâche n'a pas été lancée");
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   #############################################################################
 | |
| 
 | |
|   static function action(?string $title, ?int $maxSteps=null): void {
 | |
|     self::get()->getRunfile()->action($title, $maxSteps);
 | |
|   }
 | |
| 
 | |
|   static function step(int $nbSteps=1): void {
 | |
|     self::get()->getRunfile()->step($nbSteps);
 | |
|   }
 | |
| }
 |