405 lines
		
	
	
		
			11 KiB
		
	
	
	
		
			PHP
		
	
	
	
	
	
			
		
		
	
	
			405 lines
		
	
	
		
			11 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\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 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,
 | 
						|
      ];
 | 
						|
    });
 | 
						|
  }
 | 
						|
 | 
						|
  /**
 | 
						|
   * 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);
 | 
						|
  }
 | 
						|
}
 |