nur-sery/wip/os/proc/ManagedTask.php

577 lines
17 KiB
PHP
Raw Normal View History

2024-03-22 13:20:05 +04:00
<?php
2024-04-04 22:57:10 +04:00
namespace nur\sery\wip\os\proc;
2024-03-22 13:20:05 +04:00
use nur\sery\php\coll\AutoArray;
/**
* Class ManagedTask: une tâche de fond
*
* --autogen-properties-and-methods--
*/
class ManagedTask extends AutoArray {
const LOCK = "task";
const DEFINITION_SCHEMA = [
"id" => ["string", null, "identifiant de la tâche"],
"serial" => ["int", null, "numéro de série permettant de distinguer deux occurrences de la tâche"],
"title" => ["?string", null, "description de la tâche"],
"valid" => ["bool", false, "la tâche est-elle valide?"],
"owner_login" => ["?string", null, "compte de la personne qui a lancé la tâche"],
"owner_name" => ["?string", null, "nom de la personne qui a lancé la tâche"],
"owner_page" => ["?string", null, "page qui a créé cette tâche"],
"owner_params" => ["?array", null, "paramètres à passer à la page"],
"cmd" => [null, null, "commande à lancer"],
"logfile" => ["?string", null, "sortie de la commande"],
];
const SCHEMA = [
"definition" => [
"?array", null, "définition de la tâche",
"schema" => self::DEFINITION_SCHEMA,
],
"state" => [
"?array", null, "instance de la tâche",
"schema" => [
"definition" => [
"array", null, "copie de la définition de la tâche",
"schema" => self::DEFINITION_SCHEMA,
],
"started" => ["bool", false, "la tâche a-t-elle été démarrée?"],
"date_start" => ["?datetime", null, "date du démarrage de la tâche"],
"pid" => ["?int", null, "PID du process contrôleur"],
"status" => ["?string", null, "Message de statut indiqué par la tâche"],
"stopped" => ["bool", false, "la tâche est-elle terminée?"],
"date_stop" => ["?datetime", null, "date de l'arrêt de la tâche"],
"retcode" => ["?int", null, "code de retour de la commande"],
"done" => ["bool", false, "la fin de la tâche a-t-elle été prise en compte?"],
],
],
"" => [
"auto_properties" => [
"id" => "definition.id",
"serial" => "definition.serial",
"title" => "definition.title",
"valid" => "definition.valid",
"owner_login" => "definition.owner_login",
"owner_name" => "definition.owner_name",
"owner_page" => "definition.owner_page",
"owner_params" => "definition.owner_params",
"cmd" => "definition.cmd",
"logfile" => "definition.logfile",
]
],
];
const _AUTO_PROPERTIES = self::SCHEMA[""]["auto_properties"];
const _SCHEMA = [
"id" => ["string", null, "identifiant de la tâche"],
"serial" => ["string", null, "numéro de série permettant de distinguer deux occurrences de la tâche"],
"title" => ["?string", null, "description de la tâche"],
"valid" => ["bool", false, "la tâche est-elle valide?"],
"owner_login" => ["?string", null, "compte de la personne qui a lancé la tâche"],
"owner_name" => ["?string", null, "nom de la personne qui a lancé la tâche"],
"page" => ["?array", null, "page qui a créé cette tâche et paramètres à passer à la page"],
"cmd" => [null, null, "commande à lancer"],
"logfile" => ["?string", null, "sortie de commande"],
"started" => ["bool", false, "la tâche a-t-elle été démarrée?"],
"date_start" => ["?datetime", null, "date du démarrage de la tâche"],
"pid" => ["?int", null, "PID du process contrôleur"],
"stopped" => ["bool", false, "la tâche est-elle terminée?"],
"date_stop" => ["?datetime", null, "date de l'arrêt de la tâche"],
"retcode" => ["?int", null, "code de retour de la commande"],
"done" => ["bool", false, "la fin de la tâche a-t-elle été prise en compte?"],
];
function __construct(string $id, bool $autoUpdate=false, ?callable $init=null) {
# ne pas appeler parent::__construct()
if (file_exists($id)) {
$file = $id;
} else {
$authz = authz::get();
$this->data = [
"id" => $id,
"serial" => 0,
"owner_login" => $authz->getUsername(),
"owner_name" => $authz->getDisplayName(),
];
$file = tasks::pf("$id.task");
}
$this->init = $init;
$this->file = $file;
$this->ensureTask($autoUpdate);
}
/** @var ?callable */
protected $init;
/** @var string */
protected $file;
private function ensureTask(bool $autoUpdate): void {
lock::exlusive(self::LOCK);
try {
if (is_file($this->file)) {
$this->_reload();
} else {
$this->_reset();
$this->_save();
}
$logfile = $this->getLogfile();
if ($logfile !== null) os::mkdirof($logfile);
} finally {
lock::release(self::LOCK);
}
if ($autoUpdate) $this->update();
}
private function _init(): bool {
$init = $this->init;
if ($init !== null) {
func::call($init, $this);
return true;
}
return false;
}
function init(): void {
if ($this->init !== null) {
lock::exlusive(self::LOCK);
try {
$this->_init();
$this->_save();
} finally {
lock::release(self::LOCK);
}
}
}
private function _reset(): void {
$authz = authz::get();
$id = $this->data["id"];
$serial = A::get($this->data, "serial", 0);
$this->data = $this->ensureData([
"id" => $id,
"serial" => $serial + 1,
"owner_login" => $authz->getUsername(),
"owner_name" => $authz->getDisplayName(),
"logfile" => logs::pf("$id/latest.log"),
]);
$this->_init();
}
function reset(): void {
lock::exlusive(self::LOCK);
try {
$this->_reset();
$this->_save();
} finally {
lock::release(self::LOCK);
}
}
private function _reload(): void {
$this->data = unserialize(file_get_contents($this->file));
}
function reload(): void {
lock::exlusive(self::LOCK);
try {
$this->_reload();
} finally {
lock::release(self::LOCK);
}
}
private function _save(): void {
os::mkdirof($this->file);
$outf = fopen($this->file, "w+");
fwrite($outf, serialize($this->data));
fclose($outf);
}
function save(): void {
lock::exlusive(self::LOCK);
try {
$this->_save();
} finally {
lock::release(self::LOCK);
}
}
/** vérifier que l'objet est bien initialisé */
function validate(): void {
if (!$this->isValid()) {
if ($this->getCmd() === null) {
throw new ValueException("cmd is required");
}
$this->setValid(true);
$this->save();
}
}
function _launch(): void {
$args = [
__DIR__.'/../../lib/launch_task.php',
"--envname", envs::get(),
$this->getId(),
];
$logfile = $this->getLogfile();
if ($logfile !== null) A::merge($args, ["--logfile", $logfile]);
$cmd = new Cmd($args);
$cmd->addRedir("null");
$cmd->passthru();
$this->reload();
}
function isLaunchable(): bool {
return $this->isStarted();
}
function launch(): void {
if (!$this->isStartable()) return;
if ($this->isDone()) $this->reset();
$this->_launch();
}
function isUpdatable(): bool {
return $this->isLaunchable() && !$this->isDone();
}
function update(): void {
if ($this->isUpdatable()) $this->_launch();
else $this->init();
}
function kill(): void {
if (!$this->isStarted() || $this->isStopped()) return;
$args = [
__DIR__.'/../../lib/launch_task.php',
"-e", envs::get(),
"--kill",
$this->getId(),
];
$logfile = $this->getLogfile();
if ($logfile !== null) A::merge($args, ["-L", $logfile]);
$cmd = new Cmd($args);
$cmd->addRedir("null");
$cmd->passthru();
$this->reload();
}
function isStartable(): bool {
return !$this->isStarted() || $this->isDone();
}
/**
* démarrer la commande. doit être lancé depuis launch_task.php
*/
function ltStart(?string $logfile): void {
$pid = pcntl_fork();
if ($pid == -1) {
# parent, impossible de forker
throw new IllegalAccessException("unable to fork");
} elseif ($pid) {
# parent, fork ok
$this->setStarted(true);
$this->setDateStart(date::datetime());
$this->setPid($pid);
$this->save();
} else {
## child, fork ok
# Créer un groupe de process, pour pouvoir les tuer toutes en même temps
posix_setsid();
msg::push($oldMsg, null, [
"output" => $logfile,
]);
$retcode = -776;
try {
# tout d'abord synchroniser les fichiers le cas échéant
$command = $this->get("command");
$append = false;
if ($command !== null) {
$files = $command["files"];
$forceSync = $this->get("force_sync");
files::sync($files, $forceSync, $logfile, "wb");
$append = true;
}
# puis lancer la commande
$cmd = Cmd::with($this->getCmd());
if ($logfile !== null) $cmd->addRedir("both", $logfile, $append);
$cmd->fork_exec($retcode);
} catch (Exception $e) {
msg::error($e);
} finally {
$this->reload();
$this->setStopped(true);
$this->setDateStop(date::datetime());
$this->setRetcode($retcode);
$this->save();
msg::pop($oldMsg);
}
}
}
/** arrêter la commande. doit être lancé depuis launch_task.php */
function ltKill(?string $logfile): void {
msg::push($oldMsg, null, $logfile);
try {
$id = $this->getId();
$pid = $this->getPid();
msg::action("$id: $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 ($this->ltIsUndead()) {
sleep(1);
if (--$timeout == 0) {
msg::afailure("tentative d'arrêt de la tâche");
return;
}
}
msg::asuccess("tâche arrêtée");
$this->setStopped(true);
$this->setDateStop(date::datetime());
$this->setRetcode(-787);
$this->setDone(true);
$this->save();
} finally {
msg::pop($oldMsg);
}
}
function isReapable(): bool {
return $this->isStopped() && !$this->isDone();
}
/**
* marquer la commande comme terminée. doit être lancé depuis launch_task.php
*/
function ltReap(): void {
pcntl_waitpid($this->getPid(), $status);
$this->setDone(true);
$this->save();
}
/**
* vérifier si on est dans le cas la tâche est censée tourner mais en
* réalité ce n'est pas le cas. doit être lancé depuis launch_task.php
*/
function ltIsUndead(): bool {
if (!posix_kill($this->getPid(), 0)) {
switch (posix_get_last_error()) {
case PCNTL_ESRCH:
# process inexistant
return true;
case PCNTL_EPERM:
# process auquel on n'a pas accès: ce doit être un autre process qui a
# réutilisé le PID
return true;
case PCNTL_EINVAL:
# ne devrait pas se produire
return false;
}
}
# process existant
return false;
}
/**
* marquer la tâche comme terminée avec un code d'erreur si elle n'existe
* plus. doit être lancé depuis launch_task.php
*/
function ltCleanUndead(): void {
if (!$this->isStopped()) {
$this->setStopped(true);
$this->setDateStop(date::datetime());
$this->setRetcode(-777);
}
$this->setDone(true);
$this->save();
}
function getIdTitle(): string {
$idTitle = $this->getId();
$title = $this->getTitle();
if ($title) $idTitle .= " -- $title";
return $idTitle;
}
function getNameOrLogin(): string {
$nameOrLogin = $this->getOwnerName();
if ($nameOrLogin === null) $nameOrLogin = $this->getOwnerLogin();
if ($nameOrLogin === null) $nameOrLogin = "(unknown)";
return $nameOrLogin;
}
const MAX_LOG_SIZE = 256 * 1024;
const CACTION_NONE = "n";
const CACTION_REPLACE = "r";
const CACTION_UPDATE = "u";
function export(?int $serial=null, ?int $cs=null, ?int $ce=null): array {
$task = $this->array();
$dateStart = new Datetime($this->getDateStart());
$dateStop = new Datetime($this->getDateStop());
# $ca = action à faire par le client: replace ou update
# ls = local start, le = local end (local === server en l'occurrence)
# cs = client start, ce = client end (pour CACTION_REPLACE)
# $ps = plus start, $pe = plus end (pour CACTION_UPDATE)
# $rs = read start, $re = read end
if ($serial !== null && $cs !== null && $ce !== null && $this->isStarted()) {
lock::exlusive(self::LOCK);
$inf = false;
try {
$logfile = $this->getLogfile();
if (!file_exists($logfile)) {
# s'assurer que le fichier existe (il peut avoir été nettoyé entre temps)
f::close(f::open($logfile, "cb"));
}
$inf = f::open($logfile, "rb");
$le = f::seek($inf, 0, SEEK_END);
$ls = $le - self::MAX_LOG_SIZE;
if ($ls <= 0) {
$ls = 0;
} else {
# trouver le premier saut de ligne
$ls = f::find_nl($inf, $ls);
}
if ($serial != $this->getSerial()) {
# nouvelle tâche, on recommence tout
$rs = $cs = $ls;
$re = $ce = $le;
$ca = self::CACTION_REPLACE;
} elseif ($ls <= $cs) {
# cas courant, on rajoute du contenu, mais pas plus que MAX_LOG_SIZE
$ls = $cs;
$ps = 0;
$pe = $le - $ce;
$rs = $ce;
$re = $le;
$ca = self::CACTION_UPDATE;
$cs = $ps;
$ce = $pe;
} elseif ($ls <= $ce) {
# on a dépassé MAX_LOG_SIZE, il faut recalculer
# garder une partie des logs précédents
$ps = $ls - $cs;
$pe = $le - $ce;
$rs = $ce;
$re = $le;
$ca = self::CACTION_UPDATE;
$cs = $ps;
$ce = $pe;
} else {
# ne rien garder des logs précédents
$rs = $cs = $ls;
$re = $ce = $le;
$ca = self::CACTION_REPLACE;
}
$logSize = $re - $rs;
if ($logSize > 0) {
f::seek($inf, $rs, SEEK_SET);
$log = f::read($inf, $logSize);
$lf = new BaseF(); #XXX
$lf->formatContent($log);
} elseif ($ca == self::CACTION_REPLACE) {
$log = "";
} else {
$log = false;
}
} finally {
if ($inf) f::close($inf);
lock::release(self::LOCK);
}
} else {
$cs = $ce = false;
$ca = self::CACTION_NONE;
$log = false;
}
$page = $this->getPage();
if ($page !== null) {
$dest = A::get($page, 0);
$params = A::get($page, 1);
$pageUrl = page::bu($dest, $params);
} else {
$pageUrl = false;
}
A::merge($task, [
"id_title" => $this->getIdTitle(),
"name_or_login" => $this->getNameOrLogin(),
"page_url" => $pageUrl,
"elapsed_start" => $dateStart->getElapsed()->formatAt(),
"elapsed_stop" => $dateStop->getElapsed()->formatSince(),
"elapsed_total" => $dateStart->getElapsed($dateStop)->formatDelay(),
"launchable" => $this->isLaunchable(),
"updatable" => $this->isUpdatable(),
"startable" => $this->isStartable(),
"reapable" => $this->isReapable(),
"working" => $this->isStarted() && !$this->isDone(),
"ok" => $this->isDone() && $this->getRetcode() == 0,
"ko" => $this->isDone() && $this->getRetcode() != 0,
"log" => $log,
]);
return [$task, $ca, $cs, $ce];
}
#############################################################################
const _AUTOGEN_CONSTS = [
"_AUTO_GETTERS" => [Autogen::class, "auto_getters", self::SCHEMA],
"_AUTO_SETTERS" => [Autogen::class, "auto_setters", self::SCHEMA],
];
const _AUTOGEN_METHODS = [
[Autogen::class, "auto_getters_methods", self::SCHEMA],
[Autogen::class, "auto_setters_methods", self::SCHEMA],
];
const _AUTO_GETTERS = /*autogen*/[
'getId' => 'id',
'getSerial' => 'serial',
'getTitle' => 'title',
'isValid' => 'valid',
'getOwnerLogin' => 'owner_login',
'getOwnerName' => 'owner_name',
'getPage' => 'page',
'getCmd' => 'cmd',
'getLogfile' => 'logfile',
'isStarted' => 'started',
'getDateStart' => 'date_start',
'getPid' => 'pid',
'isStopped' => 'stopped',
'getDateStop' => 'date_stop',
'getRetcode' => 'retcode',
'isDone' => 'done',
];
const _AUTO_SETTERS = /*autogen*/[
'setId' => 'id',
'setSerial' => 'serial',
'setTitle' => 'title',
'setValid' => 'valid',
'setOwnerLogin' => 'owner_login',
'setOwnerName' => 'owner_name',
'setPage' => 'page',
'setCmd' => 'cmd',
'setLogfile' => 'logfile',
'setStarted' => 'started',
'setDateStart' => 'date_start',
'setPid' => 'pid',
'setStopped' => 'stopped',
'setDateStop' => 'date_stop',
'setRetcode' => 'retcode',
'setDone' => 'done',
];
}