From 333ddca4f5037521bdbf3b8248e6aba90bfa9ceb Mon Sep 17 00:00:00 2001 From: Jephte Clain Date: Thu, 26 Sep 2024 17:31:36 +0400 Subject: [PATCH] modifs.mineures sans commentaires --- src/os/proc/AbstractCmd.php | 4 +- src/os/sh.php | 19 ++- src/tools/BgLauncherApp.php | 14 ++- src/tools/SteamTrainApp.php | 11 +- wip/app/RunFile.php | 238 ++++++++++++++++++++---------------- wip/app/app2.php | 18 ++- wip/app/cli/Application.php | 14 ++- wip/app/cli/bg_launcher.php | 129 ++++++++----------- 8 files changed, 240 insertions(+), 207 deletions(-) diff --git a/src/os/proc/AbstractCmd.php b/src/os/proc/AbstractCmd.php index 0560634..942cc83 100644 --- a/src/os/proc/AbstractCmd.php +++ b/src/os/proc/AbstractCmd.php @@ -193,9 +193,9 @@ abstract class AbstractCmd implements ICmd { return $retcode == 0; } - function fork_exec(int &$retcode=null): bool { + function fork_exec(?int &$retcode=null, bool $wait=true): bool { $cmd = $this->getCmd(null, true); - sh::_fork_exec($cmd, $retcode); + sh::_fork_exec($cmd, $retcode, $wait); return $retcode == 0; } } diff --git a/src/os/sh.php b/src/os/sh.php index f1e9891..91bf370 100644 --- a/src/os/sh.php +++ b/src/os/sh.php @@ -136,27 +136,34 @@ class sh { return $retcode == 0; } + static function _waitpid(int $pid, ?int &$retcode=null): bool { + pcntl_waitpid($pid, $status); + if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status); + elseif (pcntl_wifsignaled($status)) $retcode = -pcntl_wtermsig($status); + else $retcode = app2::EC_FORK_CHILD; + return $retcode == 0; + } + /** * Lancer la commande $cmd dans un processus fils via un shell et attendre la * fin de son exécution. * * $cmd doit déjà être formaté comme il convient */ - static final function _fork_exec(string $cmd, int &$retcode=null): bool { + static final function _fork_exec(string $cmd, ?int &$retcode=null, bool $wait=true): bool { $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 - pcntl_waitpid($pid, $status); - if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status); - else $retcode = app2::EC_FORK_CHILD; - return $retcode == 0; + if ($wait) return self::_waitpid($pid, $retcode); + $retcode = null; + return true; } // child, fork ok pcntl_exec("/bin/sh", ["-c", $cmd]); - return false; + throw StateException::unexpected_state(); } /** diff --git a/src/tools/BgLauncherApp.php b/src/tools/BgLauncherApp.php index 34deace..f3bf360 100644 --- a/src/tools/BgLauncherApp.php +++ b/src/tools/BgLauncherApp.php @@ -1,9 +1,12 @@ getDesc(), $level); + msg::print(yaml::with(["data" => $runfile->read()]), -1); + } + function main() { $args = $this->args; @@ -52,18 +60,20 @@ class BgLauncherApp extends Application { switch ($this->action) { case self::ACTION_START: $appClass::_manage_runfile(count($args), $args, $runfile); + if ($runfile->warnIfLocked()) return; array_splice($args, 0, 0, [ PHP_BINARY, - NULIB_APP_app_launcher, + path::abspath(NULIB_APP_app_launcher), ]); app2::params_putenv(); bg_launcher::_start($args, $runfile); break; case self::ACTION_STOP: bg_launcher::_stop($runfile); + self::show_infos($runfile); break; case self::ACTION_INFOS: - yaml::dump($runfile->read()); + self::show_infos($runfile); break; } } diff --git a/src/tools/SteamTrainApp.php b/src/tools/SteamTrainApp.php index c05dc95..a5fb681 100644 --- a/src/tools/SteamTrainApp.php +++ b/src/tools/SteamTrainApp.php @@ -18,10 +18,19 @@ class SteamTrainApp extends Application { "description" => << 1, + "help" => "spécifier le nombre d'étapes", + ], + ["-s", "--use-signal-handler", "value" => true, + "help" => "installer un gestionnaire de signaux", + ], ]; + protected $count = 100; + function main() { - $count = 100; + $count = intval($this->count); app2::action("Running train...", $count); for ($i = 1; $i <= $count; $i++) { msg::print("Tchou-tchou! x $i"); diff --git a/wip/app/RunFile.php b/wip/app/RunFile.php index 8afd663..b429d63 100644 --- a/wip/app/RunFile.php +++ b/wip/app/RunFile.php @@ -1,11 +1,13 @@ $this->name, - "id" => bin2hex(random_bytes(16)), - "pg_pid" => null, - "pid" => $pid, + "mode" => null, + "pgid" => null, + "pid" => null, "serial" => 0, # lock "locked" => false, @@ -60,10 +56,11 @@ class RunFile { "date_release" => null, # run "logfile" => $this->logfile, - "date_start" => $dateStart, + "date_start" => null, "date_stop" => null, "exitcode" => null, - "is_done" => null, + "is_reaped" => null, + "is_ack_done" => null, # action "action" => null, "action_date_start" => null, @@ -75,7 +72,7 @@ class RunFile { function read(): array { $data = $this->file->unserialize(); - if (!is_array($data)) $data = $this->initData(false); + if (!is_array($data)) $data = $this->initData(); return $data; } @@ -84,7 +81,7 @@ class RunFile { $file->lockWrite(); $data = $file->unserialize(null, false, true); if (!is_array($data)) { - $data = $this->initData(false); + $data = $this->initData(); $file->ftruncate(); $file->serialize($data, false, true); } @@ -160,14 +157,40 @@ class RunFile { # cycle de vie de l'application /** - * indiquer que l'application démarre. l'état est entièrement réinitialisé, - * sauf le PID du leader qui est laissé en l'état + * Préparer le démarrage de l'application. Cette méhode est appelée par un + * script externe qui doit préparer le démarrage du script + * + * - démarrer un groupe de process dont le process courant est le leader */ + function wfPrepare(?int &$pgid=null): void { + $this->update(function (array $data) use (&$pgid) { + posix_setsid(); + $pgid = posix_getpid(); + return cl::merge($this->initData(), [ + "mode" => "session", + "pgid" => $pgid, + "pid" => null, + "date_start" => new DateTime(), + ]); + }); + } + + /** indiquer que l'application démarre. */ function wfStart(): void { $this->update(function (array $data) { - return cl::merge($this->initData(), [ - "pg_pid" => $data["pg_pid"], - ]); + $pid = posix_getpid(); + if ($data["mode"] === "session") { + A::merge($data, [ + "pid" => $pid, + ]); + } else { + $data = cl::merge($this->initData(), [ + "mode" => "standalone", + "pid" => $pid, + "date_start" => new DateTime(), + ]); + } + return $data; }); } @@ -183,13 +206,12 @@ class RunFile { return $data["date_start"] !== null && $data["date_stop"] === null; } - /** - * vérifier si l'application marquée comme démarrée tourne réellement - */ - function isRunning(?array $data=null): bool { - $data ??= $this->read(); - if ($data["date_start"] === null) return false; - if ($data["date_stop"] !== null) return false; + function _getCid(array $data=null): int { + if ($data["mode"] === "session") return -$data["pgid"]; + else return $data["pid"]; + } + + function _isRunning(array $data=null): bool { if (!posix_kill($data["pid"], 0)) { switch (posix_get_last_error()) { case 1: #PCNTL_EPERM: @@ -208,10 +230,22 @@ class RunFile { return true; } + /** + * vérifier si l'application marquée comme démarrée tourne réellement + */ + function isRunning(?array $data=null): bool { + $data ??= $this->read(); + if ($data["date_start"] === null) return false; + if ($data["date_stop"] !== null) return false; + return $this->_isRunning($data); + } + /** indiquer que l'application s'arrête */ function wfStop(): void { $this->update(function (array $data) { - return ["date_stop" => new DateTime()]; + return [ + "date_stop" => new DateTime(), + ]; }); } @@ -228,16 +262,45 @@ class RunFile { } /** après l'arrêt de l'application, mettre à jour le code de retour */ - function wfStopped(int $exitcode): void { + function wfReaped(int $exitcode): void { $this->update(function (array $data) use ($exitcode) { return [ - "pg_pid" => null, + "mode" => null, + "pgid" => null, "date_stop" => $data["date_stop"] ?? new DateTime(), "exitcode" => $exitcode, + "is_reaped" => true, ]; }); } + /** + * vérifier si on est dans le cas où la tâche devrait tourner mais en réalité + * ce n'est pas le cas + */ + function _isUndead(?int $pid=null): bool { + $data = $this->read(); + if ($data["date_start"] === null) return false; + if ($data["date_stop"] !== null) return false; + $pid ??= $data["pid"]; + if (!posix_kill($pid, 0)) { + switch (posix_get_last_error()) { + case 1: #PCNTL_EPERM: + # process auquel on n'a pas accès?! est-ce un autre process qui a + # réutilisé le PID? + return false; + case 3: #PCNTL_ESRCH: + # process inexistant + return true; + case 22: #PCNTL_EINVAL: + # ne devrait pas se produire + return false; + } + } + # process existant auquel on a accès + return false; + } + /** * comme {@link self::isStopped()} mais ne renvoie true qu'une seule fois si * $updateDone==true @@ -246,16 +309,42 @@ class RunFile { $done = false; $this->update(function (array $ldata) use (&$done, &$data, $updateDone) { $data = $ldata; - if ($data["date_start"] === null || $data["date_stop"] === null || $data["is_done"]) { + if ($data["date_start"] === null || $data["date_stop"] === null || $data["is_ack_done"]) { return false; } $done = true; - if ($updateDone) return ["is_done" => $done]; + if ($updateDone) return ["is_ack_done" => $done]; else return null; }); return $done; } + function getDesc(?array $data=null): ?string { + $data ??= $this->read(); + $desc = $data["name"]; + $dateStart = $data["date_start"]; + $dateStop = $data["date_stop"]; + $exitcode = $data["exitcode"]; + if ($exitcode !== null) $exitcode = "\nCode de retour $exitcode"; + if (!$this->wasStarted($data)) { + return "$desc: pas encore démarré"; + } elseif ($this->isRunning($data)) { + $sinceStart = Elapsed::format_since($dateStart); + $started = "\nDémarré depuis $dateStart ($sinceStart)"; + return "$desc: EN COURS pid $data[pid]$started"; + } elseif ($this->isStopped($data)) { + $duration = "\nDurée ".Elapsed::format_delay($dateStart, $dateStop); + $sinceStop = Elapsed::format_since($dateStop); + $stopped = "\nArrêtée $sinceStop le $dateStop"; + $reaped = $data["is_reaped"]? ", reaped": null; + $done = $data["is_ack_done"]? ", ACK done": null; + return "$desc: TERMINEE$duration$stopped$exitcode$reaped$done"; + } else { + $stopped = $dateStop? "\nArrêtée le $dateStop": null; + return "$desc: CRASHED\nCommencé le $dateStart$stopped$exitcode"; + } + } + #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # gestion des actions @@ -286,21 +375,20 @@ class RunFile { function getActionDesc(?array $data=null): ?string { $data ??= $this->read(); $action = $data["action"]; - if ($action === null) { - return "pid $data[pid] [$data[date_start]]"; + if ($action !== null) { + $date ??= $data["action_date_step"]; + $date ??= $data["action_date_start"]; + if ($date !== null) $action = "$date $action"; + $action = "Etape en cours: $action"; + $current = $data["action_current_step"]; + $max = $data["action_max_step"]; + if ($current !== null && $max !== null) { + $action .= " ($current / $max)"; + } elseif ($current !== null) { + $action .= " ($current)"; + } } - - $date ??= $data["action_date_step"]; - $date ??= $data["action_date_start"]; - if ($date !== null) $action = "[$date] $action"; - $current = $data["action_current_step"]; - $max = $data["action_max_step"]; - if ($current !== null && $max !== null) { - $action .= " ($current / $max)"; - } elseif ($current !== null) { - $action .= " ($current)"; - } - return "pid $data[pid] $action"; + return $action; } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ @@ -313,64 +401,4 @@ class RunFile { $name = str::join("/", [$this->name, $name]); return new LockFile($file, $name, $title); } - - #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ - # Gestionnaire de tâches (tm_*) - - /** démarrer un groupe de process dont le process courant est le leader */ - function tm_startPg(): void { - $this->update(function (array $data) { - posix_setsid(); - return [ - "pg_pid" => posix_getpid(), - ]; - }); - } - - /** - * vérifier si on est dans le cas où la tâche devrait tourner mais en réalité - * ce n'est pas le cas - */ - function tm_isUndead(?int $pid=null): bool { - $data = $this->read(); - if ($data["date_start"] === null) return false; - if ($data["date_stop"] !== null) return false; - $pid ??= $data["pid"]; - if (!posix_kill($pid, 0)) { - switch (posix_get_last_error()) { - case 1: #PCNTL_EPERM: - # process auquel on n'a pas accès?! est-ce un autre process qui a - # réutilisé le PID? - return false; - case 3: #PCNTL_ESRCH: - # process inexistant - return true; - case 22: #PCNTL_EINVAL: - # ne devrait pas se produire - return false; - } - } - # process existant auquel on a accès - return false; - } - - function tm_isReapable(): bool { - $data = $this->read(); - return $data["date_stop"] !== null && $data["exitcode"] === null; - } - - /** marquer la tâche comme terminée */ - function tm_reap(?int $pid=null): void { - $data = $this->read(); - $pid ??= $data["pid"]; - pcntl_waitpid($pid, $status); - $exitcode = pcntl_wifexited($status)? pcntl_wexitstatus($status): 127; - $this->update(function (array $data) use ($exitcode) { - return [ - "pg_pid" => null, - "date_stop" => $data["date_stop"] ?? new DateTime(), - "exitcode" => $data["exitcode"] ?? $exitcode, - ]; - }); - } } diff --git a/wip/app/app2.php b/wip/app/app2.php index 8a49a88..d10508d 100644 --- a/wip/app/app2.php +++ b/wip/app/app2.php @@ -1,6 +1,7 @@ getDesc(); if ($runfile->isRunning()) { - $action = $runfile->getActionDesc(); - if ($action !== null) $action = ": $action"; - echo "running$action\n"; + $actionDesc = $runfile->getActionDesc(); + if ($actionDesc !== null) $actionDesc = "\n$actionDesc"; + echo "$desc$actionDesc\n"; } else { - echo "not running\n"; + echo "$desc\n"; $ec = 1; } break; @@ -113,7 +114,7 @@ abstract class Application { static function run(?Application $app=null): void { $unlock = false; $stop = false; - register_shutdown_function(function() use (&$unlock, &$stop) { + $shutdown = function () use (&$unlock, &$stop) { if ($unlock) { app2::get()->getRunfile()->release(); $unlock = false; @@ -122,7 +123,8 @@ abstract class Application { app2::get()->getRunfile()->wfStop(); $stop = false; } - }); + }; + register_shutdown_function($shutdown); app2::install_signal_handler(static::USE_SIGNAL_HANDLER); try { static::_initialize_app(); diff --git a/wip/app/cli/bg_launcher.php b/wip/app/cli/bg_launcher.php index a5c9861..c0148a9 100644 --- a/wip/app/cli/bg_launcher.php +++ b/wip/app/cli/bg_launcher.php @@ -2,95 +2,38 @@ namespace nur\sery\wip\app\cli; use nur\sery\ExitError; -use nur\sery\file\TmpfileWriter; -use nur\sery\os\path; use nur\sery\os\proc\Cmd; +use nur\sery\os\sh; use nur\sery\output\msg; use nur\sery\wip\app\app2; -use nur\sery\wip\app\args; use nur\sery\wip\app\RunFile; class bg_launcher { - /** - * transformer une liste d'argument de la forme - * - ["myArg" => $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; + static function _start(array $args, Runfile $runfile): void { $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(); + } elseif (!$pid) { + # child, fork ok + $runfile->wfPrepare($pid); $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; + $cmd->fork_exec($exitcode, false); + sh::_waitpid(-$pid, $exitcode); } finally { - $runfile->wfStopped($exitcode); + $runfile->wfReaped($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)) { + private static function kill(int $pid, int $signal): bool { + if (!posix_kill($pid, $signal)) { switch (posix_get_last_error()) { case PCNTL_ESRCH: msg::afailure("process inexistant"); @@ -102,17 +45,53 @@ class bg_launcher { msg::afailure("signal invalide"); break; } - return; + return false; } + return true; + } + + static function _stop(Runfile $runfile): bool { + $data = $runfile->read(); + $pid = $runfile->_getCid($data); + $stopped = false; + msg::action("term $pid"); $timeout = 10; - while ($runfile->tm_isUndead($pid)) { - sleep(1); - if (--$timeout == 0) { - msg::afailure("impossible d'arrêter la tâche"); - return; + $delay = 300000; + while (--$timeout >= 0) { + if (!self::kill($pid, SIGTERM)) { + msg::afailure(); + return false; + } + usleep($delay); + $delay = 1000000; // attendre 1 seconde à partir de la deuxième fois + if (!$runfile->_isRunning($data)) { + msg::asuccess(); + $stopped = true; + break; } } - $runfile->wfStopped(app2::EC_REAPABLE); - msg::asuccess(); + if (!$stopped) { + msg::action("kill $pid"); + $timeout = 3; + $delay = 300000; + while (--$timeout >= 0) { + if (!self::kill($pid, SIGKILL)) { + msg::afailure(); + return false; + } + usleep($delay); + $delay = 1000000; // attendre 1 seconde à partir de la deuxième fois + if (!$runfile->_isRunning($data)) { + msg::asuccess(); + $stopped = true; + break; + } + } + } + if ($stopped) { + sh::_waitpid($pid, $exitcode); + $runfile->wfReaped($exitcode); + } + return $stopped; } }