modifs.mineures sans commentaires

This commit is contained in:
Jephté Clain 2024-06-13 07:28:11 +04:00
parent a7ac59ec9c
commit efdf473097
12 changed files with 596 additions and 26 deletions

59
lib/launch.php Executable file
View File

@ -0,0 +1,59 @@
#!/usr/bin/php
<?php
require $_composer_autoload_path?? __DIR__.'/../vendor/autoload.php';
use nur\cli\Application;
use nur\sery\wip\app\app;
use nur\sery\wip\app\launcher;
use nur\yaml;
Application::run(new class extends Application {
const ACTION_INFOS = 0, ACTION_START = 1, ACTION_STOP = 2;
const ARGS = [
"merge" => 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;
}
}
});

View File

@ -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;

View File

@ -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 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,
];
});
}
}

View File

@ -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];

View File

@ -1,8 +1,8 @@
<?php
namespace nur\sery\text;
use nur\b\ValueException;
use nur\txt;
use nur\sery\ValueException;
use nur\sery\txt;
/**
* Class Word: accord d'un nom ou d'un adjectif en genre et en nombre

294
src/txt.php Normal file
View File

@ -0,0 +1,294 @@
<?php
namespace nur\sery;
/**
* Class str: gestion des chaines de caractère multi-octets
*/
class txt {
/**
* Retourner $s converti en chaine non nulle, ou "" si $s est fausse (cela
* n'inclue pas la chaine "0")
*/
static final function with($s): string {
if (!is_string($s)) {
if (!$s) return "";
else $s = strval($s);
}
return $s;
}
/**
* Retourner $s converti en chaine non nulle, ou "" si $s est fausse selon les
* règles de PHP
*/
static final function pwith($s): string {
if (!is_string($s)) {
if (!$s) return "";
else $s = strval($s);
}
return $s?: "";
}
/**
* tronquer si nécessaire $s à la valeur $length.
* la chaine $suffix est rajoutée le cas échéant de façon que la taille
* totale n'excède pas $length caractères.
*
* si $ellips est true et que le troncage est nécessaire, remplacer les 3
* derniers caractères par "..."
*/
static final function trunc(?string $s, int $length, bool $ellips=false, ?string $suffix=null): ?string {
if ($s === null) return null;
if ($suffix !== null) $length -= mb_strlen($suffix);
if (mb_strlen($s) > $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);
}
}
}

View File

@ -1,12 +1,20 @@
<?php
namespace nur\sery\app;
namespace nur\sery\wip\app;
#XXX déplacer dans nur\sery\app dès que la dépendance sur nur\cli\Application sera levée
use nur\cli\Application;
use nur\sery\app\LockFile;
use nur\sery\app\RunFile;
use nur\sery\os\path;
use nur\sery\os\sh;
use nur\sery\ValueException;
class app {
private static function isa_Application($app): bool {
if (!is_string($app)) return false;
return $app === Application::class || is_subclass_of($app, Application::class);
}
/** @param Application|string */
static function with($app): self {
if ($app instanceof Application) {
@ -18,7 +26,7 @@ class app {
$etcdir = $app::ETCDIR;
$vardir = $app::VARDIR;
$logdir = $app::LOGDIR;
} elseif (is_string($app) && is_subclass_of($app, Application::class)) {
} elseif (self::isa_Application($app)) {
$projdir = constant("$app::PROJDIR");
$appcode = constant("$app::APPCODE");
$name = constant("$app::NAME");

80
src/wip/app/launcher.php Normal file
View File

@ -0,0 +1,80 @@
<?php
namespace nur\sery\wip\app;
use Exception;
use nur\b\proc\Cmd;
use nur\sery\app\RunFile;
use nur\sery\output\msg;
use nur\sery\StateException;
use nur\yaml;
class launcher {
static function launch(string $appClass, ...$args): int {
$cmd = new Cmd([
__DIR__."/../../../lib/launch.php",
$appClass,
...$args,
]);
$cmd->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();
}
}

1
tbin/.gitignore vendored
View File

@ -1 +1,2 @@
/output-forever.log
/devel/

7
tbin/long-task.php Executable file
View File

@ -0,0 +1,7 @@
#!/usr/bin/php
<?php
require(__DIR__.'/../vendor/autoload.php');
use nur\sery\app\LongTaskApp;
LongTaskApp::run();

20
tests/app/LongTaskApp.php Normal file
View File

@ -0,0 +1,20 @@
<?php
namespace nur\sery\app;
use nur\cli\Application;
use nur\sery\output\msg;
class LongTaskApp extends Application {
const APPCODE = "long-task";
const USE_LOGFILE = true;
const USE_RUNFILE = true;
const USE_RUNLOCK = true;
function main() {
$step = 100;
while (--$step > 0) {
msg::print("step $step");
sleep(1);
}
}
}

View File

@ -0,0 +1,7 @@
<?php
namespace nur\sery\app;
use nulib\tests\TestCase;
class launcherTest extends TestCase {
}