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);
    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" => "PROFILS D'EXECUTION",
    ["group",
      ["-p", "--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", config::PROD]],
      ["-T", "--test", "action" => [app::class, "set_profile", config::TEST]],
      ["--devel", "action" => [app::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 = app::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();
  static function runfile(): RunFile {
    return app::with(static::class)->getRunfile();
  }
}