diff --git a/lib/launch.php b/lib/launch.php new file mode 100755 index 0000000..de58548 --- /dev/null +++ b/lib/launch.php @@ -0,0 +1,59 @@ +#!/usr/bin/php + parent::ARGS, + "purpose" => "lancer une tâche de fond", + "usage" => "ApplicationClass args...", + + ["-s", "--start", "name" => "action", "value" => self::ACTION_START, + "help" => "démarrer la tâche, c'est la valeur par défaut" + ], + ["-k", "--stop", "name" => "action", "value" => self::ACTION_STOP, + "help" => "arrêter la tâche" + ], + ["-i", "--infos", "name" => "action", "value" => self::ACTION_INFOS, + "help" => "afficher des informations sur la tâche" + ], + ]; + + protected $action = self::ACTION_START; + + protected $args; + + function main() { + $appClass = $this->args[0] ?? null; + if ($appClass === null) { + self::die("Vous devez spécifier la classe de l'application"); + } + $args = array_slice($this->args, 1); + + $useRunfile = constant("$appClass::USE_RUNFILE"); + if (!$useRunfile) { + self::die("Cette application ne supporte pas l'usage de runfile"); + } + + $app = app::with($appClass); + $runfile = $app->getRunfile(); + switch ($this->action) { + case self::ACTION_START: + launcher::_start($args, $runfile); + break; + case self::ACTION_STOP: + launcher::_stop($runfile); + break; + case self::ACTION_INFOS: + yaml::dump($runfile->read()); + break; + } + } +}); diff --git a/nur_src/cli/Application.php b/nur_src/cli/Application.php index a3d3b80..e0906c8 100644 --- a/nur_src/cli/Application.php +++ b/nur_src/cli/Application.php @@ -9,7 +9,7 @@ use nur\config\ArrayConfig; use nur\msg; use nur\os; use nur\path; -use nur\sery\app\app; +use nur\sery\wip\app\app; use nur\sery\output\log as nlog; use nur\sery\output\msg as nmsg; use nur\sery\output\console as nconsole; diff --git a/src/app/RunFile.php b/src/app/RunFile.php index 151a74f..b5a5ae4 100644 --- a/src/app/RunFile.php +++ b/src/app/RunFile.php @@ -40,6 +40,7 @@ class RunFile { return [ "name" => $this->name, "id" => bin2hex(random_bytes(16)), + "pg_pid" => null, "pid" => posix_getpid(), "serial" => 0, "date_start" => $dateStart, @@ -127,45 +128,65 @@ class RunFile { 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); + } + /** indiquer que l'application démarre */ function start(): void { - $this->file->serialize($this->initData()); + $this->update(function (array $data) { + # garder l'identifiant de process + $pgPid = $data["pg_pid"] ?? null; + return cl::merge($this->initData(), [ + "pg_pid" => $pgPid, + ]); + }); } /** indiquer le début d'une action */ function action(?string $title, ?int $maxSteps=null): void { - [$file, $data] = $this->willWrite(); - $file->serialize(self::merge($data, [ - "action" => $title, - "action_date_start" => new DateTime(), - "action_max_step" => $maxSteps, - "action_current_step" => 0, - ])); + $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 { - [$file, $data] = $this->willWrite(); - $file->serialize(self::merge($data, [ - "action_date_step" => new DateTime(), - "action_current_step" => $data["action_current_step"] + $nbSteps, - ])); + $this->update(function (array $data) use ($nbSteps) { + return [ + "action_date_step" => new DateTime(), + "action_current_step" => $data["action_current_step"] + $nbSteps, + ]; + }); } /** indiquer que l'application s'arrête */ function stop(): void { - [$file, $data] = $this->willWrite(); - $file->serialize(self::merge($data, [ - "date_stop" => new DateTime(), - ])); + $this->update(function (array $data) { + return ["date_stop" => new DateTime()]; + }); } /** après l'arrêt de l'application, mettre à jour le code de retour */ function stopped(int $exitcode): void { - [$file, $data] = $this->willWrite(); - $file->serialize(self::merge($data, [ - "exitcode" => $exitcode, - ])); + $this->update(function (array $data) use ($exitcode) { + return [ + "pg_pid" => null, + "date_stop" => $data["date_stop"] ?? new DateTime(), + "exitcode" => $exitcode, + ]; + }); } function getLockFile(?string $name=null, ?string $title=null): LockFile { @@ -175,4 +196,61 @@ class RunFile { $name = str::join("/", [$this->name, $name]); return new LockFile($file, $name, $title); } + + /** démarrer un groupe de process */ + 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 PCNTL_ESRCH: + # process inexistant + return true; + case PCNTL_EPERM: + # process auquel on n'a pas accès?! est-ce un autre process qui a + # réutilisé le PID? + return false; + case 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, + ]; + }); + } } diff --git a/src/str.php b/src/str.php index a289c68..257344a 100644 --- a/src/str.php +++ b/src/str.php @@ -242,6 +242,22 @@ class str { return true; } + /** + * ajouter $sep$prefix$text$suffix à $s si $text est non vide + * + * NB: ne rajouter $sep que si $s est non vide + */ + static final function addsep(?string &$s, ?string $sep, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + if (!$text) return; + if ($s) $s .= $sep; + $s .= $prefix.$text.$suffix; + } + + /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */ + static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + self::addsep($s, " ", $text, $prefix, $suffix); + } + /** splitter $s en deux chaines séparées par $sep */ static final function split_pair(?string $s, string $sep=":"): array { if ($s === null) return [null, null]; diff --git a/src/text/Word.php b/src/text/Word.php index 7d79728..7a826dc 100644 --- a/src/text/Word.php +++ b/src/text/Word.php @@ -1,8 +1,8 @@ $length) { + if ($ellips && $length > 3) $s = mb_substr($s, 0, $length - 3)."..."; + else $s = mb_substr($s, 0, $length); + } + if ($suffix !== null) $s .= $suffix; + return $s; + } + + /** trimmer $s */ + static final function trim(?string $s): ?string { + if ($s === null) return null; + return trim($s); + } + + /** trimmer $s à gauche */ + static final function ltrim(?string $s): ?string { + if ($s === null) return null; + return ltrim($s); + } + + /** trimmer $s à droite */ + static final function rtrim(?string $s): ?string { + if ($s === null) return null; + return rtrim($s); + } + + static final function left(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size); + } + + static final function right(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, " ", STR_PAD_LEFT); + } + + static final function center(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, " ", STR_PAD_BOTH); + } + + static final function pad0(?string $s, int $size): ?string { + if ($s === null) return null; + return mb_str_pad($s, $size, "0", STR_PAD_LEFT); + } + + static final function lower(?string $s): ?string { + if ($s === null) return null; + return mb_strtolower($s); + } + + static final function lower1(?string $s): ?string { + if ($s === null) return null; + return mb_strtolower(mb_substr($s, 0, 1)).mb_substr($s, 1); + } + + static final function upper(?string $s): ?string { + if ($s === null) return null; + return mb_strtoupper($s); + } + + static final function upper1(?string $s): ?string { + if ($s === null) return null; + return mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + } + + static final function upperw(?string $s, ?string $delimiters=null): ?string { + if ($s === null) return null; + if ($delimiters === null) $delimiters = " _-\t\r\n\f\v"; + $pattern = "/([".preg_quote($delimiters)."])/u"; + $words = preg_split($pattern, $s, -1, PREG_SPLIT_DELIM_CAPTURE); + $max = count($words) - 1; + $ucwords = []; + for ($i = 0; $i < $max; $i += 2) { + $s = $words[$i]; + $ucwords[] = mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + $ucwords[] = $words[$i + 1]; + } + $s = $words[$max]; + $ucwords[] = mb_strtoupper(mb_substr($s, 0, 1)).mb_substr($s, 1); + return implode("", $ucwords); + } + + protected static final function _starts_with(string $prefix, string $s, ?int $min_len=null): bool { + if ($prefix === $s) return true; + $len = mb_strlen($prefix); + if ($min_len !== null && ($len < $min_len || $len > mb_strlen($s))) return false; + return $len == 0 || $prefix === mb_substr($s, 0, $len); + } + + /** + * tester si $s commence par $prefix + * par exemple: + * - starts_with("", "whatever") est true + * - starts_with("fi", "first") est true + * - starts_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - starts_with("a", "abc", 2) est false + * - starts_with("a", "a", 2) est true + */ + static final function starts_with(?string $prefix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $prefix === null) return false; + else return self::_starts_with($prefix, $s, $min_len); + } + + /** Retourner $s sans le préfixe $prefix s'il existe */ + static final function without_prefix(?string $prefix, ?string $s): ?string { + if ($s === null || $prefix === null) return $s; + if (self::_starts_with($prefix, $s)) $s = mb_substr($s, mb_strlen($prefix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le préfixe $prefix s'il existe + * + * retourner true si le préfixe a été enlevé. + */ + static final function del_prefix(?string &$s, ?string $prefix): bool { + if ($s === null || !self::_starts_with($prefix, $s)) return false; + $s = self::without_prefix($prefix, $s); + return true; + } + + /** + * Retourner $s avec le préfixe $prefix + * + * Si $unless_exists, ne pas ajouter le préfixe s'il existe déjà + */ + static final function with_prefix(?string $prefix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $prefix === null) return $s; + if (!self::_starts_with($prefix, $s) || !$unless_exists) $s = $prefix.$sep.$s; + return $s; + } + + /** + * modifier $s en place pour ajouter le préfixe $prefix + * + * retourner true si le préfixe a été ajouté. + */ + static final function add_prefix(?string &$s, ?string $prefix, bool $unless_exists=true): bool { + if (($s === null || self::_starts_with($prefix, $s)) && $unless_exists) return false; + $s = self::with_prefix($prefix, $s, null, $unless_exists); + return true; + } + + protected static final function _ends_with(string $suffix, string $s, ?int $min_len=null): bool { + if ($suffix === $s) return true; + $len = mb_strlen($suffix); + if ($min_len !== null && ($len < $min_len || $len > mb_strlen($s))) return false; + return $len == 0 || $suffix === mb_substr($s, -$len); + } + + /** + * tester si $string se termine par $suffix + * par exemple: + * - ends_with("", "whatever") est true + * - ends_with("st", "first") est true + * - ends_with("no", "yes") est false + * + * si $min_len n'est pas null, c'est la longueur minimum requise de $prefix + * pour qu'on teste la correspondance. dans le cas contraire, la valeur de + * retour est toujours false, sauf s'il y a égalité. e.g + * - ends_with("c", "abc", 2) est false + * - ends_with("c", "c", 2) est true + */ + static final function ends_with(?string $suffix, ?string $s, ?int $min_len=null): bool { + if ($s === null || $suffix === null) return false; + else return self::_ends_with($suffix, $s, $min_len); + } + + /** Retourner $s sans le suffixe $suffix s'il existe */ + static final function without_suffix(?string $suffix, ?string $s): ?string { + if ($s === null || $suffix === null) return $s; + if (self::_ends_with($suffix, $s)) $s = mb_substr($s, 0, -mb_strlen($suffix)); + return $s; + } + + /** + * modifier $s en place pour supprimer le suffixe $suffix s'il existe + * + * retourner true si le suffixe a été enlevé. + */ + static final function del_suffix(?string &$s, ?string $suffix): bool { + if ($s === null || !self::_ends_with($suffix, $s)) return false; + $s = self::without_suffix($suffix, $s); + return true; + } + + /** + * Retourner $s avec le suffixe $suffix + * + * Si $unless_exists, ne pas ajouter le suffixe s'il existe déjà + */ + static final function with_suffix(?string $suffix, ?string $s, ?string $sep=null, bool $unless_exists=false): ?string { + if ($s === null || $suffix === null) return $s; + if (!self::_ends_with($suffix, $s) || !$unless_exists) $s = $s.$sep.$suffix; + return $s; + } + + /** + * modifier $s en place pour ajouter le suffixe $suffix + * + * retourner true si le suffixe a été ajouté. + */ + static final function add_suffix(?string &$s, ?string $suffix, bool $unless_exists=true): bool { + if (($s === null || self::_ends_with($suffix, $s)) && $unless_exists) return false; + $s = self::with_suffix($suffix, $s, null, $unless_exists); + return true; + } + + /** + * ajouter $sep$prefix$text$suffix à $s si $text est non vide + * + * NB: ne rajouter $sep que si $s est non vide + */ + static final function addsep(?string &$s, ?string $sep, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + if (!$text) return; + if ($s) $s .= $sep; + $s .= $prefix.$text.$suffix; + } + + /** si $text est non vide, ajouter $prefix$text$suffix à $s en séparant la valeur avec un espace */ + static final function add(?string &$s, ?string $text, ?string $prefix=null, ?string $suffix=null): void { + self::addsep($s, " ", $text, $prefix, $suffix); + } + + ############################################################################# + # divers + + /** + * supprimer les diacritiques de la chaine $text + * + * la translitération se fait avec les règles de la locale spécifiée. + * NB: la translitération ne fonctionne pas si LC_CTYPE == C ou POISX + */ + static final function remove_diacritics(?string $text, string $locale="fr_FR.UTF-8"): ?string { + if ($text === null) return null; + #XXX est-ce thread-safe? + $olocale = setlocale(LC_CTYPE, 0); + try { + setlocale(LC_CTYPE, $locale); + $clean = @iconv("UTF-8", "US-ASCII//TRANSLIT", $text); + if ($clean === false) $clean = ""; + return $clean; + } finally { + setlocale(LC_CTYPE, $olocale); + } + } +} diff --git a/src/app/app.php b/src/wip/app/app.php similarity index 95% rename from src/app/app.php rename to src/wip/app/app.php index a3eb7d4..c472cf6 100644 --- a/src/app/app.php +++ b/src/wip/app/app.php @@ -1,12 +1,20 @@ addRedir("null"); + $cmd->passthru($exitcode); + return $exitcode; + } + + static function _start(array $args, Runfile $runfile): void { + $pid = pcntl_fork(); + if ($pid == -1) { + # parent, impossible de forker + throw new StateException("unable to fork"); + } elseif ($pid) { + # parent, fork ok + } else { + ## child, fork ok + # Créer un groupe de process, pour pouvoir les tuer toutes en même temps + $runfile->tm_startPg(); + $exitcode = -776; + try { + # puis lancer la commande + $cmd = new Cmd($args); + #XXX fichier de log? + $cmd->addRedir("null"); + $cmd->fork_exec($exitcode); + } finally { + $runfile->stopped($exitcode); + } + } + } + + static function _stop(Runfile $runfile): void { + $data = $runfile->read(); + $pid = $data["pg_pid"]; + if ($pid === null) { + msg::warning("$data[name]: groupe de process inconnu"); + return; + } + msg::action("kill $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 ($runfile->tm_isUndead($pid)) { + sleep(1); + if (--$timeout == 0) { + msg::afailure("impossible d'arrêter la tâche"); + return; + } + } + $runfile->stopped(-778); + msg::asuccess(); + } +} diff --git a/tbin/.gitignore b/tbin/.gitignore index cf227b5..74dd8df 100644 --- a/tbin/.gitignore +++ b/tbin/.gitignore @@ -1 +1,2 @@ /output-forever.log +/devel/ diff --git a/tbin/long-task.php b/tbin/long-task.php new file mode 100755 index 0000000..330c5e7 --- /dev/null +++ b/tbin/long-task.php @@ -0,0 +1,7 @@ +#!/usr/bin/php + 0) { + msg::print("step $step"); + sleep(1); + } + } +} diff --git a/tests/app/launcherTest.php b/tests/app/launcherTest.php new file mode 100644 index 0000000..a8274ac --- /dev/null +++ b/tests/app/launcherTest.php @@ -0,0 +1,7 @@ +