modifs.mineures sans commentaires

This commit is contained in:
Jephté Clain 2024-09-26 17:31:36 +04:00
parent b195888602
commit 333ddca4f5
8 changed files with 240 additions and 207 deletions

View File

@ -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;
}
}

View File

@ -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();
}
/**

View File

@ -1,9 +1,12 @@
<?php
namespace nur\sery\tools;
use nur\sery\os\path;
use nur\sery\output\msg;
use nur\sery\wip\app\app2;
use nur\sery\wip\app\cli\Application;
use nur\sery\wip\app\cli\bg_launcher;
use nur\sery\wip\app\RunFile;
use nur\yaml;
class BgLauncherApp extends Application {
@ -31,6 +34,11 @@ class BgLauncherApp extends Application {
protected ?array $args = null;
static function show_infos(RunFile $runfile, ?int $level=null): void {
msg::print($runfile->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;
}
}

View File

@ -18,10 +18,19 @@ class SteamTrainApp extends Application {
"description" => <<<EOT
Cette application peut être utilisée pour tester le lancement des tâches de fond
EOT,
["-c", "--count", "args" => 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");

View File

@ -1,11 +1,13 @@
<?php
namespace nur\sery\wip\app;
use nur\sery\A;
use nur\sery\cl;
use nur\sery\file\SharedFile;
use nur\sery\os\path;
use nur\sery\output\msg;
use nur\sery\php\time\DateTime;
use nur\sery\php\time\Elapsed;
use nur\sery\str;
/**
@ -41,18 +43,12 @@ class RunFile {
], $merge);
}
protected function initData(bool $forStart=true): array {
if ($forStart) {
$pid = posix_getpid();
$dateStart = new DateTime();
} else {
$pid = $dateStart = null;
}
protected function initData(): array {
return [
"name" => $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 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,13 +375,11 @@ 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";
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) {
@ -300,7 +387,8 @@ class RunFile {
} 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 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,
];
});
}
}

View File

@ -1,6 +1,7 @@
<?php
namespace nur\sery\wip\app;
use Closure;
use nur\sery\A;
use nur\sery\cl;
use nur\sery\ExitError;
@ -475,15 +476,12 @@ 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;
const EC_UNEXPECTED = 254;
const EC_SIGNAL = 255;
const EC_FORK_CHILD = 250;
const EC_FORK_PARENT = 251;
const EC_DISABLED = 252;
const EC_LOCKED = 253;
const EC_BAD_COMMAND = 254;
const EC_UNEXPECTED = 255;
#############################################################################
@ -492,7 +490,7 @@ class app2 {
static function install_signal_handler(bool $allow=true): void {
if (!$allow) return;
$signalHandler = function(int $signo, $siginfo) {
throw new ExitError(self::EC_SIGNAL);
throw new ExitError(128 + $signo);
};
pcntl_signal(SIGHUP, $signalHandler);
pcntl_signal(SIGINT, $signalHandler);

View File

@ -94,12 +94,13 @@ abstract class Application {
break;
case "infos":
case "i":
$desc = $runfile->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();

View File

@ -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;
}
}
$runfile->wfStopped(app2::EC_REAPABLE);
usleep($delay);
$delay = 1000000; // attendre 1 seconde à partir de la deuxième fois
if (!$runfile->_isRunning($data)) {
msg::asuccess();
$stopped = true;
break;
}
}
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;
}
}