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