nur-sery/wip/app/RunFile.php

377 lines
10 KiB
PHP

<?php
namespace nur\sery\wip\app;
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\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(bool $forStart=true): array {
if ($forStart) {
$pid = posix_getpid();
$dateStart = new DateTime();
} else {
$pid = $dateStart = null;
}
return [
"name" => $this->name,
"id" => bin2hex(random_bytes(16)),
"pg_pid" => null,
"pid" => $pid,
"serial" => 0,
# lock
"locked" => false,
"date_lock" => null,
"date_release" => null,
# run
"logfile" => $this->logfile,
"date_start" => $dateStart,
"date_stop" => null,
"exitcode" => null,
"is_done" => null,
# action
"action" => null,
"action_date_start" => null,
"action_current_step" => null,
"action_max_step" => null,
"action_date_step" => null,
];
}
function read(): array {
$data = $this->file->unserialize();
if (!is_array($data)) $data = $this->initData(false);
return $data;
}
protected function willWrite(): array {
$file = $this->file;
$file->lockWrite();
$data = $file->unserialize(null, false, true);
if (!is_array($data)) {
$data = $this->initData(false);
$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
/**
* indiquer que l'application démarre. l'état est entièrement réinitialisé,
* sauf le PID du leader qui est laissé en l'état
*/
function wfStart(): void {
$this->update(function (array $data) {
return cl::merge($this->initData(), [
"pg_pid" => $data["pg_pid"],
]);
});
}
/** 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;
}
/**
* 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;
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;
}
/** 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 wfStopped(int $exitcode): void {
$this->update(function (array $data) use ($exitcode) {
return [
"pg_pid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $exitcode,
];
});
}
/**
* 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_done"]) {
return false;
}
$done = true;
if ($updateDone) return ["is_done" => $done];
else return null;
});
return $done;
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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) {
return "pid $data[pid] [$data[date_start]]";
}
$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";
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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);
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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,
];
});
}
}