469 lines
13 KiB
PHP
469 lines
13 KiB
PHP
<?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\os\sh;
|
|
use nur\sery\output\msg;
|
|
use nur\sery\php\time\DateTime;
|
|
use nur\sery\php\time\Elapsed;
|
|
use nur\sery\str;
|
|
|
|
/**
|
|
* Class RunFile: une classe permettant de suivre le fonctionnement d'une
|
|
* application qui tourne en tâche de fond
|
|
*/
|
|
class RunFile {
|
|
const RUN_EXT = ".run";
|
|
const LOCK_EXT = ".lock";
|
|
|
|
const NAME = null;
|
|
|
|
function __construct(?string $name, string $file, ?string $logfile=null) {
|
|
$file = path::ensure_ext($file, self::RUN_EXT);
|
|
$this->name = $name ?? static::NAME;
|
|
$this->file = new SharedFile($file);
|
|
$this->logfile = $logfile;
|
|
}
|
|
|
|
protected ?string $name;
|
|
|
|
protected SharedFile $file;
|
|
|
|
protected ?string $logfile;
|
|
|
|
function getLogfile(): ?string {
|
|
return $this->logfile;
|
|
}
|
|
|
|
protected static function merge(array $data, array $merge): array {
|
|
return cl::merge($data, [
|
|
"serial" => $data["serial"] + 1,
|
|
], $merge);
|
|
}
|
|
|
|
protected function initData(): array {
|
|
return [
|
|
"name" => $this->name,
|
|
"mode" => null,
|
|
"pgid" => null,
|
|
"pid" => null,
|
|
"serial" => 0,
|
|
# lock
|
|
"locked" => false,
|
|
"date_lock" => null,
|
|
"date_release" => null,
|
|
# run
|
|
"logfile" => $this->logfile,
|
|
"date_start" => null,
|
|
"date_stop" => null,
|
|
"exitcode" => null,
|
|
"is_reaped" => null,
|
|
"is_ack_done" => null,
|
|
# action
|
|
"action" => null,
|
|
"action_date_start" => null,
|
|
"action_current_step" => null,
|
|
"action_max_step" => null,
|
|
"action_date_step" => null,
|
|
];
|
|
}
|
|
|
|
function reset(bool $delete=false) {
|
|
$file = $this->file;
|
|
if ($delete) {
|
|
$file->close();
|
|
unlink($file->getFile());
|
|
} else {
|
|
$file->ftruncate();
|
|
}
|
|
}
|
|
|
|
function read(): array {
|
|
$data = $this->file->unserialize();
|
|
if (!is_array($data)) $data = $this->initData();
|
|
return $data;
|
|
}
|
|
|
|
protected function willWrite(): array {
|
|
$file = $this->file;
|
|
$file->lockWrite();
|
|
$data = $file->unserialize(null, false, true);
|
|
if (!is_array($data)) {
|
|
$data = $this->initData();
|
|
$file->ftruncate();
|
|
$file->serialize($data, false, true);
|
|
}
|
|
return [$file, $data];
|
|
}
|
|
|
|
protected function serialize(SharedFile $file, array $data, ?array $merge=null): void {
|
|
$file->ftruncate();
|
|
$file->serialize(self::merge($data, $merge), true, true);
|
|
}
|
|
|
|
protected function update(callable $func): void {
|
|
/** @var SharedFile$file */
|
|
[$file, $data] = $this->willWrite();
|
|
$merge = call_user_func($func, $data);
|
|
if ($merge !== null && $merge !== false) {
|
|
$this->serialize($file, $data, $merge);
|
|
} else {
|
|
$file->cancelWrite();
|
|
}
|
|
}
|
|
|
|
function haveWorked(int $serial, ?int &$currentSerial=null, ?array $data=null): bool {
|
|
$data ??= $this->read();
|
|
$currentSerial = $data["serial"];
|
|
return $serial !== $currentSerial;
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# verrouillage par défaut
|
|
|
|
function isLocked(?array &$data=null): bool {
|
|
$data = $this->read();
|
|
return $data["locked"];
|
|
}
|
|
|
|
function warnIfLocked(?array $data=null): bool {
|
|
$data ??= $this->read();
|
|
if ($data["locked"]) {
|
|
msg::warning("$data[name]: possède le verrou depuis $data[date_lock]");
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
function lock(): bool {
|
|
$this->update(function ($data) use (&$locked) {
|
|
if ($data["locked"]) {
|
|
$locked = false;
|
|
return null;
|
|
} else {
|
|
$locked = true;
|
|
return [
|
|
"locked" => true,
|
|
"date_lock" => new DateTime(),
|
|
"date_release" => null,
|
|
];
|
|
}
|
|
});
|
|
return $locked;
|
|
}
|
|
|
|
function release(): void {
|
|
$this->update(function ($data) {
|
|
return [
|
|
"locked" => false,
|
|
"date_release" => new DateTime(),
|
|
];
|
|
});
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# cycle de vie de l'application
|
|
|
|
/**
|
|
* 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) {
|
|
$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;
|
|
});
|
|
}
|
|
|
|
/** tester si l'application a déjà été démarrée au moins une fois */
|
|
function wasStarted(?array $data=null): bool {
|
|
$data ??= $this->read();
|
|
return $data["date_start"] !== null;
|
|
}
|
|
|
|
/** tester si l'application est démarrée et non arrêtée */
|
|
function isStarted(?array $data=null): bool {
|
|
$data ??= $this->read();
|
|
return $data["date_start"] !== null && $data["date_stop"] === null;
|
|
}
|
|
|
|
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:
|
|
# 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 false;
|
|
case 22: #PCNTL_EINVAL:
|
|
# ne devrait pas se produire
|
|
return false;
|
|
}
|
|
}
|
|
# process existant auquel on a accès
|
|
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(),
|
|
];
|
|
});
|
|
}
|
|
|
|
/** tester si l'application est déjà été stoppée au moins une fois */
|
|
function wasStopped(?array $data=null): bool {
|
|
$data ??= $this->read();
|
|
return $data["date_stop"] !== null;
|
|
}
|
|
|
|
/** tester si l'application a été démarrée puis arrêtée */
|
|
function isStopped(?array $data=null): bool {
|
|
$data ??= $this->read();
|
|
return $data["date_start"] !== null && $data["date_stop"] !== null;
|
|
}
|
|
|
|
/** après l'arrêt de l'application, mettre à jour le code de retour */
|
|
function wfReaped(int $exitcode): void {
|
|
$this->update(function (array $data) use ($exitcode) {
|
|
return [
|
|
"mode" => null,
|
|
"pgid" => null,
|
|
"date_stop" => $data["date_stop"] ?? new DateTime(),
|
|
"exitcode" => $exitcode,
|
|
"is_reaped" => true,
|
|
];
|
|
});
|
|
}
|
|
|
|
private static function kill(int $pid, int $signal, ?string &$reason=null): bool {
|
|
if (!posix_kill($pid, $signal)) {
|
|
switch (posix_get_last_error()) {
|
|
case PCNTL_ESRCH:
|
|
$reason = "process inexistant";
|
|
break;
|
|
case PCNTL_EPERM:
|
|
$reason = "process non accessible";
|
|
break;
|
|
case PCNTL_EINVAL:
|
|
$reason = "signal invalide";
|
|
break;
|
|
}
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
function wfKill(?string &$reason=null): bool {
|
|
$data = $this->read();
|
|
$pid = $this->_getCid($data);
|
|
$stopped = false;
|
|
$timeout = 10;
|
|
$delay = 300000;
|
|
while (--$timeout >= 0) {
|
|
if (!self::kill($pid, SIGTERM, $reason)) return false;
|
|
usleep($delay);
|
|
$delay = 1000000; // attendre 1 seconde à partir de la deuxième fois
|
|
if (!$this->_isRunning($data)) {
|
|
$stopped = true;
|
|
break;
|
|
}
|
|
}
|
|
if (!$stopped) {
|
|
$timeout = 3;
|
|
$delay = 300000;
|
|
while (--$timeout >= 0) {
|
|
if (!self::kill($pid, SIGKILL, $reason)) return false;
|
|
usleep($delay);
|
|
$delay = 1000000; // attendre 1 seconde à partir de la deuxième fois
|
|
if (!$this->_isRunning($data)) {
|
|
$stopped = true;
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
if ($stopped) {
|
|
sh::_waitpid($pid, $exitcode);
|
|
$this->wfReaped($exitcode);
|
|
}
|
|
return $stopped;
|
|
}
|
|
|
|
/**
|
|
* 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
|
|
*/
|
|
function isDone(?array &$data=null, bool $updateDone=true): bool {
|
|
$done = false;
|
|
$this->update(function (array $ldata) use (&$done, &$data, $updateDone) {
|
|
$data = $ldata;
|
|
if ($data["date_start"] === null || $data["date_stop"] === null || $data["is_ack_done"]) {
|
|
return false;
|
|
}
|
|
$done = true;
|
|
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
|
|
|
|
/** indiquer le début d'une action */
|
|
function action(?string $title, ?int $maxSteps=null): void {
|
|
$this->update(function (array $data) use ($title, $maxSteps) {
|
|
return [
|
|
"action" => $title,
|
|
"action_date_start" => new DateTime(),
|
|
"action_max_step" => $maxSteps,
|
|
"action_current_step" => 0,
|
|
];
|
|
});
|
|
app2::_dispatch_signals();
|
|
}
|
|
|
|
/** indiquer qu'une étape est franchie dans l'action en cours */
|
|
function step(int $nbSteps=1): void {
|
|
$this->update(function (array $data) use ($nbSteps) {
|
|
return [
|
|
"action_date_step" => new DateTime(),
|
|
"action_current_step" => $data["action_current_step"] + $nbSteps,
|
|
];
|
|
});
|
|
app2::_dispatch_signals();
|
|
}
|
|
|
|
function getActionDesc(?array $data=null): ?string {
|
|
$data ??= $this->read();
|
|
$action = $data["action"];
|
|
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)";
|
|
}
|
|
}
|
|
return $action;
|
|
}
|
|
|
|
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
# Divers
|
|
|
|
function getLockFile(?string $name=null, ?string $title=null): LockFile {
|
|
$ext = self::LOCK_EXT;
|
|
if ($name !== null) $ext = ".$name$ext";
|
|
$file = path::ensure_ext($this->file->getFile(), $ext, self::RUN_EXT);
|
|
$name = str::join("/", [$this->name, $name]);
|
|
return new LockFile($file, $name, $title);
|
|
}
|
|
}
|