diff --git a/.idea/nur-sery.iml b/.idea/nur-sery.iml index 6c8130a..35cbd28 100644 --- a/.idea/nur-sery.iml +++ b/.idea/nur-sery.iml @@ -1,6 +1,7 @@ - + + diff --git a/bin/bg-launcher.php b/bin/bg-launcher.php new file mode 100755 index 0000000..8d8fe0c --- /dev/null +++ b/bin/bg-launcher.php @@ -0,0 +1,67 @@ +#!/usr/bin/php + "lancer un script en tâche de fond", + "usage" => "ApplicationClass args...", + + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "Afficher des informations sur la tâche", + ], + ["-s", "--start", "name" => "action", "value" => self::ACTION_START, + "help" => "Démarrer la tâche", + ], + ["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP, + "help" => "Arrêter la tâche", + ], + ]; + + protected int $action = self::ACTION_START; + + protected ?array $args = null; + + function main() { + $appClass = $this->args[0] ?? null; + if ($appClass === null) { + self::die("Vous devez spécifier la classe de l'application"); + } + $appClass = str_replace("/", "\\", $appClass); + if (!class_exists($appClass)) { + self::die("$appClass: classe non trouvée"); + } + $args = array_slice($this->args, 1); + + $useRunfile = constant("$appClass::USE_RUNFILE"); + if (!$useRunfile) { + self::die("Cette application ne supporte le lancement en tâche de fond"); + } + + $runfile = app2::with($appClass)->getRunfile(); + switch ($this->action) { + case self::ACTION_START: + bg_launcher::_start($args, $runfile); + break; + case self::ACTION_STOP: + bg_launcher::_stop($runfile); + break; + case self::ACTION_INFOS: + yaml::dump($runfile->read()); + break; + } + } +} + +$params = app2::params_getenv(); +if ($params !== null) app2::init($params); +BgLauncherApp::run(); diff --git a/composer.json b/composer.json index b7690fd..6b029ff 100644 --- a/composer.json +++ b/composer.json @@ -65,6 +65,7 @@ }, "bin": [ "lib/_launch.php", + "bin/bg-launcher.php", "nur_bin/compctl.php", "nur_bin/compdep.php", "nur_bin/cachectl.php", diff --git a/nur_bin/steam-train.php b/nur_bin/steam-train.php index 86e5496..dbde68a 100755 --- a/nur_bin/steam-train.php +++ b/nur_bin/steam-train.php @@ -2,6 +2,6 @@ self::TITLE, + "description" => <<getRunfile(); - $runfile->action("Running train...", 100); - for ($i = 1; $i <= 100; $i++) { + $count = 100; + app2::action("Running train...", $count); + for ($i = 1; $i <= $count; $i++) { msg::print("Tchou-tchou! x $i"); - $runfile->step(); + app2::step(); sleep(1); } } diff --git a/tests/wip/app/argsTest.php b/tests/wip/app/argsTest.php new file mode 100644 index 0000000..bea16f0 --- /dev/null +++ b/tests/wip/app/argsTest.php @@ -0,0 +1,25 @@ + false])); + self::assertSame(["--opt"], args::from_array(["opt" => true])); + self::assertSame(["--opt", "value"], args::from_array(["opt" => "value"])); + self::assertSame(["--opt", "42"], args::from_array(["opt" => 42])); + self::assertSame(["--opt", "1", "2", "3", "--"], args::from_array(["opt" => [1, 2, 3]])); + + self::assertSame(["x", "1", "2", "3", "y"], args::from_array(["x", [1, 2, 3], "y"])); + } +} diff --git a/wip/app/RunFile.php b/wip/app/RunFile.php index 2ea5791..8afd663 100644 --- a/wip/app/RunFile.php +++ b/wip/app/RunFile.php @@ -269,6 +269,7 @@ class RunFile { "action_current_step" => 0, ]; }); + app2::_dispatch_signals(); } /** indiquer qu'une étape est franchie dans l'action en cours */ diff --git a/wip/app/app2.php b/wip/app/app2.php index bb07549..cbe4984 100644 --- a/wip/app/app2.php +++ b/wip/app/app2.php @@ -6,7 +6,6 @@ use nur\sery\cl; use nur\sery\ExitError; use nur\sery\os\path; use nur\sery\os\sh; -use nur\sery\output\msg; use nur\sery\php\func; use nur\sery\str; use nur\sery\ValueException; @@ -476,6 +475,10 @@ class app2 { ############################################################################# + const EC_UNDEAD = 247; + const EC_REAPABLE = 248; + const EC_FORK_CHILD = 249; + const EC_FORK_PARENT = 250; const EC_DISABLED = 251; const EC_LOCKED = 252; const EC_BAD_COMMAND = 253; @@ -536,4 +539,14 @@ class app2 { 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); + } } diff --git a/wip/app/args.php b/wip/app/args.php new file mode 100644 index 0000000..c5179b9 --- /dev/null +++ b/wip/app/args.php @@ -0,0 +1,39 @@ + $value] devient ["--my-arg", "$value"] + * - ["myOpt" => true] devient ["--my-opt"] + * - ["myOpt" => false] est omis + * - les autres valeurs sont prises telles quelles + */ + static function from_array(?array $array): array { + $args = []; + if ($array === null) return $args; + $index = 0; + foreach ($array as $arg => $value) { + if ($value === false) continue; + if ($arg === $index) { + $index++; + } else { + $arg = str::us2camel($arg); + $arg = str::camel2us($arg, false, "-"); + $arg = str_replace("_", "-", $arg); + $args[] = "--$arg"; + if (is_array($value)) $value[] = "--"; + elseif ($value === true) $value = null; + } + if (is_array($value)) { + A::merge($args, array_map("strval", $value)); + } elseif ($value !== null) { + $args[] = "$value"; + } + } + return $args; + } +} diff --git a/wip/app/cli/bg_launcher.php b/wip/app/cli/bg_launcher.php index 559fd79..98b2bfa 100644 --- a/wip/app/cli/bg_launcher.php +++ b/wip/app/cli/bg_launcher.php @@ -1,5 +1,119 @@ $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 { + $args = args::from_array($args); + # 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 ExitError(app2::EC_FORK_PARENT, "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_app_start.log"; + $pid = posix_getpid(); + $exitcode = app2::EC_FORK_CHILD; + 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(app2::EC_REAPABLE); + msg::asuccess(); + } } diff --git a/wip/app/launcher.php b/wip/app/launcher.php deleted file mode 100644 index 4d4a299..0000000 --- a/wip/app/launcher.php +++ /dev/null @@ -1,137 +0,0 @@ - $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(); - } -}