file = new SharedFile($file); $this->name = $name ?? static::NAME; } /** @var SharedFile */ protected $file; /** @var ?string */ protected $name; protected static function merge(array $data, array $merge): array { return cl::merge($data, [ "serial" => $data["serial"] + 1, ], $merge); } protected function initData(bool $withDateStart=true): array { $dateStart = $withDateStart? new DateTime(): null; return [ "name" => $this->name, "id" => bin2hex(random_bytes(16)), "pg_pid" => null, "pid" => posix_getpid(), "serial" => 0, # lock "locked" => false, "date_lock" => null, "date_release" => null, # run "date_start" => $dateStart, "date_stop" => null, "exitcode" => 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(); $file->ftruncate(); $file->serialize($data, false, true); } $file->ftruncate(); return [$file, $data]; } protected function serialize(SharedFile $file, array $data, ?array $merge=null): void { $file->serialize(self::merge($data, $merge), true, true); } protected function update(callable $func): void { [$file, $data] = $this->willWrite(); $merge = call_user_func($func, $data); $this->serialize($file, $data, $merge); } function haveWorked(int $serial, ?int &$currentSerial=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 { if ($data === null) $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 */ function wasStarted(): bool { $data = $this->read(); return $data["date_start"] !== null; } /** tester si l'application est démarrée et non arrêtée */ function isStarted(): bool { $data = $this->read(); return $data["date_start"] !== null && $data["date_stop"] === null; } /** * vérifier si la tâche tourne et est accessible */ 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 */ function wasStopped(): bool { $data = $this->read(); return $data["date_stop"] !== null; } /** tester si l'application a été démarrée puis arrêtée */ function isStopped(): 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, ]; }); } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # 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, ]; }); } /** 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, ]; }); } #~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ # 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, ]; }); } }