377 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			377 lines
		
	
	
		
			10 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
<?php
 | 
						|
namespace nur\sery\wip\app\cli;
 | 
						|
 | 
						|
use Exception;
 | 
						|
use nur\cli\ArgsException;
 | 
						|
use nur\cli\ArgsParser;
 | 
						|
use nur\config;
 | 
						|
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;
 | 
						|
use nur\sery\wip\web\content\v;
 | 
						|
use nur\yaml;
 | 
						|
 | 
						|
/**
 | 
						|
 * 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 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.
 | 
						|
   *
 | 
						|
   * 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
 | 
						|
kill
 | 
						|
release
 | 
						|
infos
 | 
						|
dump
 | 
						|
reset
 | 
						|
 | 
						|
EOT);
 | 
						|
      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;
 | 
						|
    case "release":
 | 
						|
    case "rl":
 | 
						|
      $runfile->release();
 | 
						|
      break;
 | 
						|
    case "infos":
 | 
						|
    case "i":
 | 
						|
      $desc = $runfile->getDesc();
 | 
						|
      if ($runfile->isRunning()) {
 | 
						|
        $actionDesc = $runfile->getActionDesc();
 | 
						|
        if ($actionDesc !== null) $actionDesc = "\n$actionDesc";
 | 
						|
        echo "$desc$actionDesc\n";
 | 
						|
      } else {
 | 
						|
        echo "$desc\n";
 | 
						|
        $ec = 1;
 | 
						|
      }
 | 
						|
      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;
 | 
						|
    default:
 | 
						|
      $ec = self::_error("$argv[1]: unexpected command", app2::EC_BAD_COMMAND);
 | 
						|
    }
 | 
						|
    exit($ec);
 | 
						|
  }
 | 
						|
 | 
						|
  static function run(?Application $app=null): void {
 | 
						|
    $unlock = false;
 | 
						|
    $stop = false;
 | 
						|
    $shutdown = function () use (&$unlock, &$stop) {
 | 
						|
      if ($unlock) {
 | 
						|
        app2::get()->getRunfile()->release();
 | 
						|
        $unlock = false;
 | 
						|
      }
 | 
						|
      if ($stop) {
 | 
						|
        app2::get()->getRunfile()->wfStop();
 | 
						|
        $stop = false;
 | 
						|
      }
 | 
						|
    };
 | 
						|
    register_shutdown_function($shutdown);
 | 
						|
    app2::install_signal_handler(static::INSTALL_SIGNAL_HANDLER);
 | 
						|
    try {
 | 
						|
      static::_initialize_app();
 | 
						|
      $useRunfile = static::USE_RUNFILE;
 | 
						|
      $useRunlock = static::USE_RUNLOCK;
 | 
						|
      if ($useRunfile) {
 | 
						|
        $runfile = app2::get()->getRunfile();
 | 
						|
 | 
						|
        global $argc, $argv;
 | 
						|
        self::_manage_runfile($argc, $argv, $runfile);
 | 
						|
        if ($useRunlock && $runfile->warnIfLocked()) exit(app2::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(app2::EC_UNEXPECTED);
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  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)
 | 
						|
   *
 | 
						|
   * é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" => [app2::class, "set_profile"],
 | 
						|
        "help" => "spécifier le profil d'exécution",
 | 
						|
      ],
 | 
						|
      ["-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]],
 | 
						|
    ],
 | 
						|
  ];
 | 
						|
 | 
						|
  const VERBOSITY_SECTION = [
 | 
						|
    "title" => "NIVEAU D'INFORMATION",
 | 
						|
    "show" => false,
 | 
						|
    ["group",
 | 
						|
      ["--verbosity",
 | 
						|
        "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"]],
 | 
						|
    ],
 | 
						|
    ["-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 {
 | 
						|
    $console = console::get();
 | 
						|
    switch ($verbosity) {
 | 
						|
    case "Q":
 | 
						|
    case "silent":
 | 
						|
      $console->resetParams([
 | 
						|
        "min_level" => msg::NONE,
 | 
						|
      ]);
 | 
						|
      break;
 | 
						|
    case "q":
 | 
						|
    case "quiet":
 | 
						|
      $console->resetParams([
 | 
						|
        "min_level" => msg::MAJOR,
 | 
						|
      ]);
 | 
						|
      break;
 | 
						|
    case "n":
 | 
						|
    case "normal":
 | 
						|
      $console->resetParams([
 | 
						|
        "min_level" => msg::NORMAL,
 | 
						|
      ]);
 | 
						|
      break;
 | 
						|
    case "v":
 | 
						|
    case "verbose":
 | 
						|
      $console->resetParams([
 | 
						|
        "min_level" => msg::MINOR,
 | 
						|
      ]);
 | 
						|
      break;
 | 
						|
    case "D":
 | 
						|
    case "debug":
 | 
						|
      config::set_debug();
 | 
						|
      $console->resetParams([
 | 
						|
        "min_level" => msg::DEBUG,
 | 
						|
      ]);
 | 
						|
      break;
 | 
						|
    default:
 | 
						|
      throw ValueException::invalid_value($verbosity, "verbosity");
 | 
						|
    }
 | 
						|
  }
 | 
						|
 | 
						|
  static function set_application_log_output(string $logfile): void {
 | 
						|
    log::create_or_reset_params([
 | 
						|
      "output" => $logfile,
 | 
						|
    ], StdMessenger::class, [
 | 
						|
      "add_date" => true,
 | 
						|
      "min_level" => log::MINOR,
 | 
						|
    ]);
 | 
						|
  }
 | 
						|
  static function set_application_color(bool $color): void {
 | 
						|
    console::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 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;
 | 
						|
      }
 | 
						|
    }
 | 
						|
    $color = static::DEFAULT_PROFILE_COLOR;
 | 
						|
    return $color? "<color $color>$profile</color>": $profile;
 | 
						|
  }
 | 
						|
 | 
						|
  abstract function main();
 | 
						|
}
 |