337 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			337 lines
		
	
	
		
			9.4 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
| <?php
 | |
| namespace nulib\app\cli;
 | |
| 
 | |
| use Exception;
 | |
| use nulib\app\app;
 | |
| use nulib\app\args\AbstractArgsParser;
 | |
| use nulib\app\args\ArgsException;
 | |
| use nulib\app\args\SimpleArgsParser;
 | |
| use nulib\app\config;
 | |
| use nulib\app\RunFile;
 | |
| use nulib\ExitError;
 | |
| use nulib\ext\yaml;
 | |
| use nulib\output\console;
 | |
| use nulib\output\log;
 | |
| use nulib\output\msg;
 | |
| use nulib\output\std\StdMessenger;
 | |
| use nulib\ref\ref_profiles;
 | |
| 
 | |
| /**
 | |
|  * Class Application: application de base
 | |
|  */
 | |
| 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 MY_APP_DATADIR si le projet a
 | |
|    * pour code my-app
 | |
|    *
 | |
|    * si non définie, cette valeur est calculée automatiquement à partir de
 | |
|    * self::PROJDIR sans le suffixe "-app"
 | |
|    */
 | |
|   const PROJCODE = 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.
 | |
|    *
 | |
|    * si non définie, cette valeur est calculée automatiquement à partir de
 | |
|    * static::class
 | |
|    */
 | |
|   const NAME = null;
 | |
| 
 | |
|   /** @var string description courte de l'application */
 | |
|   const TITLE = null;
 | |
| 
 | |
|   const DATADIR = null;
 | |
|   const ETCDIR = null;
 | |
|   const VARDIR = null;
 | |
|   const LOGDIR = null;
 | |
| 
 | |
|   /** @var bool faut-il activer automatiquement l'écriture dans les logs */
 | |
|   const USE_LOGFILE = null;
 | |
| 
 | |
|   /** @var bool faut-il maintenir un fichier de suivi du process? */
 | |
|   const USE_RUNFILE = false;
 | |
| 
 | |
|   /**
 | |
|    * @var bool faut-il empêcher deux instances de cette application de se lancer
 | |
|    * en même temps?
 | |
|    *
 | |
|    * nécessite USE_RUNFILE==true
 | |
|    */
 | |
|   const USE_RUNLOCK = false;
 | |
| 
 | |
|   /** @var bool faut-il installer le gestionnaire de signaux? */
 | |
|   const INSTALL_SIGNAL_HANDLER = false;
 | |
| 
 | |
|   private static function _info(string $message, int $ec=0): int {
 | |
|     fwrite(STDERR, "INFO: $message\n");
 | |
|     return $ec;
 | |
|   }
 | |
| 
 | |
|   private static function _error(string $message, int $ec=1): int {
 | |
|     fwrite(STDERR, "ERROR: $message\n");
 | |
|     return $ec;
 | |
|   }
 | |
| 
 | |
|   static function _manage_runfile(int &$argc, array &$argv, RunFile $runfile): void {
 | |
|     if ($argc <= 1 || $argv[1] !== "//") return;
 | |
|     array_splice($argv, 1, 1); $argc--;
 | |
|     $ec = 0;
 | |
|     switch ($argv[1] ?? "infos") {
 | |
|     case "help":
 | |
|       self::_info(<<<EOT
 | |
| Valid commands:
 | |
|   infos
 | |
|   dump
 | |
|   reset
 | |
|   release
 | |
|   start
 | |
|   kill
 | |
| 
 | |
| EOT);
 | |
|       break;
 | |
|     case "infos":
 | |
|     case "i":
 | |
|       $desc = $runfile->getDesc();
 | |
|       echo implode("\n", $desc["message"])."\n";
 | |
|       $ec = $desc["exitcode"] ?? 0;
 | |
|       break;
 | |
|     case "dump":
 | |
|     case "d":
 | |
|       yaml::dump($runfile->read());
 | |
|       break;
 | |
|     case "reset":
 | |
|     case "z":
 | |
|       if (!$runfile->isRunning()) $runfile->reset();
 | |
|       else $ec = self::_error("cannot reset while running");
 | |
|       break;
 | |
|     case "release":
 | |
|     case "rl":
 | |
|       $runfile->release();
 | |
|       break;
 | |
|     case "start":
 | |
|     case "s":
 | |
|       array_splice($argv, 1, 1); $argc--;
 | |
|       return;
 | |
|     case "kill":
 | |
|     case "k":
 | |
|       if ($runfile->isRunning()) $runfile->wfKill();
 | |
|       else $ec = self::_error("not running");
 | |
|       break;
 | |
|     default:
 | |
|       $ec = self::_error("$argv[1]: unexpected command", app::EC_BAD_COMMAND);
 | |
|     }
 | |
|     exit($ec);
 | |
|   }
 | |
| 
 | |
|   static function run(?Application $app=null): void {
 | |
|     $unlock = false;
 | |
|     $stop = false;
 | |
|     $shutdown = function () use (&$unlock, &$stop) {
 | |
|       if ($unlock) {
 | |
|         app::get()->getRunfile()->release();
 | |
|         $unlock = false;
 | |
|       }
 | |
|       if ($stop) {
 | |
|         app::get()->getRunfile()->wfStop();
 | |
|         $stop = false;
 | |
|       }
 | |
|     };
 | |
|     register_shutdown_function($shutdown);
 | |
|     app::install_signal_handler(static::INSTALL_SIGNAL_HANDLER);
 | |
|     try {
 | |
|       static::_initialize_app();
 | |
|       $useRunfile = static::USE_RUNFILE;
 | |
|       $useRunlock = static::USE_RUNLOCK;
 | |
|       if ($useRunfile) {
 | |
|         $runfile = app::get()->getRunfile();
 | |
| 
 | |
|         global $argc, $argv;
 | |
|         self::_manage_runfile($argc, $argv, $runfile);
 | |
|         if ($useRunlock && $runfile->warnIfLocked()) exit(app::EC_LOCKED);
 | |
| 
 | |
|         $runfile->wfStart();
 | |
|         $stop = true;
 | |
|         if ($useRunlock) {
 | |
|           $runfile->lock();
 | |
|           $unlock = true;
 | |
|         }
 | |
|       }
 | |
|       if ($app === null) $app = new static();
 | |
|       static::_configure_app($app);
 | |
|       static::_start_app($app);
 | |
|     } catch (ExitError $e) {
 | |
|       if ($e->haveUserMessage()) msg::error($e->getUserMessage());
 | |
|       exit($e->getCode());
 | |
|     } catch (Exception $e) {
 | |
|       msg::error($e);
 | |
|       exit(app::EC_UNEXPECTED);
 | |
|     }
 | |
|   }
 | |
| 
 | |
|   protected static function _initialize_app(): void {
 | |
|     app::init(static::class);
 | |
|     app::set_fact(app::FACT_CLI_APP);
 | |
|     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" => app::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)
 | |
|    *
 | |
|    * é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" => "PROFIL D'EXECUTION",
 | |
|     "show" => false,
 | |
|     ["-c", "--config", "--app-config",
 | |
|       "args" => "file", "argsdesc" => "CONFIG.yml",
 | |
|       "action" => [config::class, "load_config"],
 | |
|       "help" => "spécifier un fichier de configuration",
 | |
|     ],
 | |
|     ["group",
 | |
|       ["-g", "--profile", "--app-profile",
 | |
|         "args" => 1, "argsdesc" => "PROFILE",
 | |
|         "action" => [app::class, "set_profile"],
 | |
|         "help" => "spécifier le profil d'exécution",
 | |
|       ],
 | |
|       ["-P", "--prod", "action" => [app::class, "set_profile", ref_profiles::PROD]],
 | |
|       ["-T", "--test", "action" => [app::class, "set_profile", ref_profiles::TEST]],
 | |
|       ["--devel", "action" => [app::class, "set_profile", ref_profiles::DEVEL]],
 | |
|     ],
 | |
|   ];
 | |
| 
 | |
|   const VERBOSITY_SECTION = [
 | |
|     "title" => "NIVEAU D'INFORMATION",
 | |
|     "show" => false,
 | |
|     ["group",
 | |
|       ["--verbosity",
 | |
|         "args" => "verbosity", "argsdesc" => "silent|quiet|verbose|debug",
 | |
|         "action" => [console::class, "set_verbosity"],
 | |
|         "help" => "spécifier le niveau d'informations affiché",
 | |
|       ],
 | |
|       ["-q", "--quiet", "action" => [console::class, "set_verbosity", "quiet"]],
 | |
|       ["-v", "--verbose", "action" => [console::class, "set_verbosity", "verbose"]],
 | |
|       ["-D", "--debug", "action" => [console::class, "set_verbosity", "debug"]],
 | |
|     ],
 | |
|     ["-L", "--logfile",
 | |
|       "args" => "output",
 | |
|       "action" => [log::class, "set_output"],
 | |
|       "help" => "Logger les messages de l'application dans le fichier spécifié",
 | |
|     ],
 | |
|     ["group",
 | |
|       ["--color",
 | |
|         "action" => [console::class, "set_color", true],
 | |
|         "help" => "Afficher (resp. ne pas afficher) la sortie en couleur par défaut",
 | |
|       ],
 | |
|       ["--no-color", "action" => [console::class, "set_color", false]],
 | |
|     ],
 | |
|   ];
 | |
| 
 | |
|   const ARGS = [
 | |
|     "sections" => [
 | |
|       self::PROFILE_SECTION,
 | |
|       self::VERBOSITY_SECTION,
 | |
|     ],
 | |
|   ];
 | |
| 
 | |
|   protected function getArgsParser(): AbstractArgsParser {
 | |
|     return new SimpleArgsParser(static::ARGS);
 | |
|   }
 | |
| 
 | |
|   /** @throws ArgsException */
 | |
|   function parseArgs(array $args=null): void {
 | |
|     $this->getArgsParser()->parse($this, $args);
 | |
|   }
 | |
| 
 | |
|   const PROFILE_COLORS = [
 | |
|     ref_profiles::PROD => "@r",
 | |
|     ref_profiles::TEST => "@g",
 | |
|     ref_profiles::DEVEL => "@w",
 | |
|   ];
 | |
|   const DEFAULT_PROFILE_COLOR = "y";
 | |
| 
 | |
|   /** retourner le profil courant en couleur */
 | |
|   static function get_profile(?string $profile=null): string {
 | |
|     if ($profile === null) $profile = app::get_profile();
 | |
|     foreach (static::PROFILE_COLORS as $text => $color) {
 | |
|       if (strpos($profile, $text) !== false) {
 | |
|         return $color? "<color $color>$profile</color>": $profile;
 | |
|       }
 | |
|     }
 | |
|     $color = static::DEFAULT_PROFILE_COLOR;
 | |
|     return $color? "<color $color>$profile</color>": $profile;
 | |
|   }
 | |
| 
 | |
|   protected ?array $args = null;
 | |
| 
 | |
|   abstract function main();
 | |
| 
 | |
|   static function runfile(): RunFile {
 | |
|     return app::with(static::class)->getRunfile();
 | |
|   }
 | |
| }
 |