mise en prod app

This commit is contained in:
Jephté Clain 2024-09-30 08:13:35 +04:00
parent fa1709216b
commit 2a20032b02
24 changed files with 342 additions and 2021 deletions

View File

@ -8,6 +8,7 @@
<sourceFolder url="file://$MODULE_DIR$/nur_src" isTestSource="false" packagePrefix="nur\" />
<sourceFolder url="file://$MODULE_DIR$/nur_tests" isTestSource="true" packagePrefix="nur\" />
<sourceFolder url="file://$MODULE_DIR$/wip" isTestSource="false" packagePrefix="nur\sery\wip\" />
<sourceFolder url="file://$MODULE_DIR$/wip_app" isTestSource="false" packagePrefix="nur\sery\app\" />
<excludeFolder url="file://$MODULE_DIR$/vendor" />
</content>
<orderEntry type="inheritedJdk" />

View File

@ -49,8 +49,9 @@
},
"autoload": {
"psr-4": {
"nur\\sery\\": "src",
"nur\\sery\\wip\\": "wip",
"nur\\sery\\app\\": "wip_app",
"nur\\sery\\": "src",
"nur\\": "nur_src"
},
"files": [

114
composer.lock generated
View File

@ -75,20 +75,20 @@
},
{
"name": "symfony/polyfill-ctype",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-ctype.git",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540"
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/0424dff1c58f028c451efff2045f5d92410bd540",
"reference": "0424dff1c58f028c451efff2045f5d92410bd540",
"url": "https://api.github.com/repos/symfony/polyfill-ctype/zipball/a3cc8b044a6ea513310cbd48ef7333b384945638",
"reference": "a3cc8b044a6ea513310cbd48ef7333b384945638",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"provide": {
"ext-ctype": "*"
@ -134,7 +134,7 @@
"portable"
],
"support": {
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-ctype/tree/v1.31.0"
},
"funding": [
{
@ -150,20 +150,20 @@
"type": "tidelift"
}
],
"time": "2024-05-31T15:07:36+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "symfony/yaml",
"version": "v5.4.40",
"version": "v5.4.44",
"source": {
"type": "git",
"url": "https://github.com/symfony/yaml.git",
"reference": "81cad0ceab3d61fe14fe941ff18a230ac9c80f83"
"reference": "7025b964f123bbf1896d7563db6ec7f1f63e918a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/yaml/zipball/81cad0ceab3d61fe14fe941ff18a230ac9c80f83",
"reference": "81cad0ceab3d61fe14fe941ff18a230ac9c80f83",
"url": "https://api.github.com/repos/symfony/yaml/zipball/7025b964f123bbf1896d7563db6ec7f1f63e918a",
"reference": "7025b964f123bbf1896d7563db6ec7f1f63e918a",
"shasum": ""
},
"require": {
@ -209,7 +209,7 @@
"description": "Loads and dumps YAML files",
"homepage": "https://symfony.com",
"support": {
"source": "https://github.com/symfony/yaml/tree/v5.4.40"
"source": "https://github.com/symfony/yaml/tree/v5.4.44"
},
"funding": [
{
@ -225,7 +225,7 @@
"type": "tidelift"
}
],
"time": "2024-05-31T14:33:22+00:00"
"time": "2024-09-16T14:36:56+00:00"
}
],
"packages-dev": [
@ -747,16 +747,16 @@
},
{
"name": "nikic/php-parser",
"version": "v5.1.0",
"version": "v5.3.0",
"source": {
"type": "git",
"url": "https://github.com/nikic/PHP-Parser.git",
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1"
"reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/683130c2ff8c2739f4822ff7ac5c873ec529abd1",
"reference": "683130c2ff8c2739f4822ff7ac5c873ec529abd1",
"url": "https://api.github.com/repos/nikic/PHP-Parser/zipball/3abf7425cd284141dc5d8d14a9ee444de3345d1a",
"reference": "3abf7425cd284141dc5d8d14a9ee444de3345d1a",
"shasum": ""
},
"require": {
@ -799,9 +799,9 @@
],
"support": {
"issues": "https://github.com/nikic/PHP-Parser/issues",
"source": "https://github.com/nikic/PHP-Parser/tree/v5.1.0"
"source": "https://github.com/nikic/PHP-Parser/tree/v5.3.0"
},
"time": "2024-07-01T20:03:41+00:00"
"time": "2024-09-29T13:56:26+00:00"
},
{
"name": "nulib/tests",
@ -955,16 +955,16 @@
},
{
"name": "phpoffice/phpspreadsheet",
"version": "1.29.0",
"version": "1.29.2",
"source": {
"type": "git",
"url": "https://github.com/PHPOffice/PhpSpreadsheet.git",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0"
"reference": "3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"reference": "fde2ccf55eaef7e86021ff1acce26479160a0fa0",
"url": "https://api.github.com/repos/PHPOffice/PhpSpreadsheet/zipball/3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f",
"reference": "3a5a818d7d3e4b5bd2e56fb9de44dbded6eae07f",
"shasum": ""
},
"require": {
@ -999,7 +999,7 @@
"phpcompatibility/php-compatibility": "^9.3",
"phpstan/phpstan": "^1.1",
"phpstan/phpstan-phpunit": "^1.0",
"phpunit/phpunit": "^8.5 || ^9.0 || ^10.0",
"phpunit/phpunit": "^8.5 || ^9.0",
"squizlabs/php_codesniffer": "^3.7",
"tecnickcom/tcpdf": "^6.5"
},
@ -1054,41 +1054,41 @@
],
"support": {
"issues": "https://github.com/PHPOffice/PhpSpreadsheet/issues",
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.0"
"source": "https://github.com/PHPOffice/PhpSpreadsheet/tree/1.29.2"
},
"time": "2023-06-14T22:48:31+00:00"
"time": "2024-09-29T07:04:47+00:00"
},
{
"name": "phpunit/php-code-coverage",
"version": "9.2.31",
"version": "9.2.32",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/php-code-coverage.git",
"reference": "48c34b5d8d983006bd2adc2d0de92963b9155965"
"reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/48c34b5d8d983006bd2adc2d0de92963b9155965",
"reference": "48c34b5d8d983006bd2adc2d0de92963b9155965",
"url": "https://api.github.com/repos/sebastianbergmann/php-code-coverage/zipball/85402a822d1ecf1db1096959413d35e1c37cf1a5",
"reference": "85402a822d1ecf1db1096959413d35e1c37cf1a5",
"shasum": ""
},
"require": {
"ext-dom": "*",
"ext-libxml": "*",
"ext-xmlwriter": "*",
"nikic/php-parser": "^4.18 || ^5.0",
"nikic/php-parser": "^4.19.1 || ^5.1.0",
"php": ">=7.3",
"phpunit/php-file-iterator": "^3.0.3",
"phpunit/php-text-template": "^2.0.2",
"sebastian/code-unit-reverse-lookup": "^2.0.2",
"sebastian/complexity": "^2.0",
"sebastian/environment": "^5.1.2",
"sebastian/lines-of-code": "^1.0.3",
"sebastian/version": "^3.0.1",
"theseer/tokenizer": "^1.2.0"
"phpunit/php-file-iterator": "^3.0.6",
"phpunit/php-text-template": "^2.0.4",
"sebastian/code-unit-reverse-lookup": "^2.0.3",
"sebastian/complexity": "^2.0.3",
"sebastian/environment": "^5.1.5",
"sebastian/lines-of-code": "^1.0.4",
"sebastian/version": "^3.0.2",
"theseer/tokenizer": "^1.2.3"
},
"require-dev": {
"phpunit/phpunit": "^9.3"
"phpunit/phpunit": "^9.6"
},
"suggest": {
"ext-pcov": "PHP extension that provides line coverage",
@ -1097,7 +1097,7 @@
"type": "library",
"extra": {
"branch-alias": {
"dev-master": "9.2-dev"
"dev-main": "9.2.x-dev"
}
},
"autoload": {
@ -1126,7 +1126,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/php-code-coverage/issues",
"security": "https://github.com/sebastianbergmann/php-code-coverage/security/policy",
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.31"
"source": "https://github.com/sebastianbergmann/php-code-coverage/tree/9.2.32"
},
"funding": [
{
@ -1134,7 +1134,7 @@
"type": "github"
}
],
"time": "2024-03-02T06:37:42+00:00"
"time": "2024-08-22T04:23:01+00:00"
},
{
"name": "phpunit/php-file-iterator",
@ -1379,16 +1379,16 @@
},
{
"name": "phpunit/phpunit",
"version": "9.6.20",
"version": "9.6.21",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/phpunit.git",
"reference": "49d7820565836236411f5dc002d16dd689cde42f"
"reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/49d7820565836236411f5dc002d16dd689cde42f",
"reference": "49d7820565836236411f5dc002d16dd689cde42f",
"url": "https://api.github.com/repos/sebastianbergmann/phpunit/zipball/de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
"reference": "de6abf3b6f8dd955fac3caad3af7a9504e8c2ffa",
"shasum": ""
},
"require": {
@ -1403,7 +1403,7 @@
"phar-io/manifest": "^2.0.4",
"phar-io/version": "^3.2.1",
"php": ">=7.3",
"phpunit/php-code-coverage": "^9.2.31",
"phpunit/php-code-coverage": "^9.2.32",
"phpunit/php-file-iterator": "^3.0.6",
"phpunit/php-invoker": "^3.1.1",
"phpunit/php-text-template": "^2.0.4",
@ -1462,7 +1462,7 @@
"support": {
"issues": "https://github.com/sebastianbergmann/phpunit/issues",
"security": "https://github.com/sebastianbergmann/phpunit/security/policy",
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.20"
"source": "https://github.com/sebastianbergmann/phpunit/tree/9.6.21"
},
"funding": [
{
@ -1478,7 +1478,7 @@
"type": "tidelift"
}
],
"time": "2024-07-10T11:45:39+00:00"
"time": "2024-09-19T10:50:18+00:00"
},
{
"name": "psr/http-client",
@ -2656,20 +2656,20 @@
},
{
"name": "symfony/polyfill-mbstring",
"version": "v1.30.0",
"version": "v1.31.0",
"source": {
"type": "git",
"url": "https://github.com/symfony/polyfill-mbstring.git",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c"
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"reference": "fd22ab50000ef01661e2a31d850ebaa297f8e03c",
"url": "https://api.github.com/repos/symfony/polyfill-mbstring/zipball/85181ba99b2345b0ef10ce42ecac37612d9fd341",
"reference": "85181ba99b2345b0ef10ce42ecac37612d9fd341",
"shasum": ""
},
"require": {
"php": ">=7.1"
"php": ">=7.2"
},
"provide": {
"ext-mbstring": "*"
@ -2716,7 +2716,7 @@
"shim"
],
"support": {
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.30.0"
"source": "https://github.com/symfony/polyfill-mbstring/tree/v1.31.0"
},
"funding": [
{
@ -2732,7 +2732,7 @@
"type": "tidelift"
}
],
"time": "2024-06-19T12:30:46+00:00"
"time": "2024-09-09T11:45:10+00:00"
},
{
"name": "theseer/tokenizer",

View File

@ -1,81 +0,0 @@
#!/usr/bin/php
<?php
$internalUse = $argv[1] ?? null;
if ($internalUse !== "--internal-use") exit("Wrong args");
$paramsfile = $argv[2] ?? null;
if (!file_exists($paramsfile)) exit("Bad params file");
$argc -= 2;
$argv = array_merge(
array_slice($argv, 0, 1),
array_slice($argv, 3),
);
$app_params = unserialize(file_get_contents($paramsfile));
@unlink($paramsfile);
require $app_params["vendor"]["autoload"];
use nur\cli\Application;
use nur\sery\output\msg;
use nur\sery\wip\app\app;
use nur\sery\app\launcher;
use nur\yaml;
class _LaunchApp extends Application {
const NAME = "_launch";
const USE_LOGFILE = true;
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) {
msg::error("Vous devez spécifier la classe de l'application");
self::die();
} elseif (!class_exists($appClass)) {
msg::error("$appClass: Cette classe n'existe pas");
self::die();
}
$args = array_slice($this->args, 1);
$useRunfile = constant("$appClass::USE_RUNFILE");
if (!$useRunfile) {
msg::error("Cette application ne supporte pas l'usage de runfile");
self::die();
}
$runfile = app::with($appClass, self::$internal_use_app_params)->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;
}
}
}
_LaunchApp::internal_use_set_app_params($app_params);
_LaunchApp::run();

View File

@ -12,7 +12,7 @@ use nur\path;
use nur\sery\app\launcher;
use nur\sery\app\RunFile;
use nur\sery\cl;
use nur\sery\wip\app\app;
use nur\sery\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

@ -1,11 +1,14 @@
<?php
namespace nur\sery\app;
use nur\sery\A;
use nur\sery\cl;
use nur\sery\file\SharedFile;
use nur\sery\os\path;
use nur\sery\os\sh;
use nur\sery\output\msg;
use nur\sery\php\time\DateTime;
use nur\sery\php\time\Elapsed;
use nur\sery\str;
/**
@ -18,21 +21,21 @@ class RunFile {
const NAME = null;
function __construct(?string $name, string $file, ?string $logfile=null) {
function __construct(?string $name, string $file, ?string $outfile=null) {
$file = path::ensure_ext($file, self::RUN_EXT);
$this->name = $name ?? static::NAME;
$this->file = new SharedFile($file);
$this->logfile = $logfile;
$this->outfile = $outfile;
}
protected ?string $name;
protected SharedFile $file;
protected ?string $logfile;
protected ?string $outfile;
function getLogfile(): ?string {
return $this->logfile;
function getOutfile(): ?string {
return $this->outfile;
}
protected static function merge(array $data, array $merge): array {
@ -41,29 +44,23 @@ class RunFile {
], $merge);
}
protected function initData(bool $forStart=true): array {
if ($forStart) {
$pid = posix_getpid();
$dateStart = new DateTime();
} else {
$pid = $dateStart = null;
}
protected function initData(): array {
return [
"name" => $this->name,
"id" => bin2hex(random_bytes(16)),
"pg_pid" => null,
"pid" => $pid,
"pgid" => null,
"pid" => null,
"serial" => 0,
# lock
"locked" => false,
"date_lock" => null,
"date_release" => null,
# run
"logfile" => $this->logfile,
"date_start" => $dateStart,
"logfile" => $this->outfile,
"date_start" => null,
"date_stop" => null,
"exitcode" => null,
"is_done" => null,
"is_reaped" => null,
"is_ack_done" => null,
# action
"action" => null,
"action_date_start" => null,
@ -73,9 +70,19 @@ class RunFile {
];
}
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(false);
if (!is_array($data)) $data = $this->initData();
return $data;
}
@ -84,7 +91,7 @@ class RunFile {
$file->lockWrite();
$data = $file->unserialize(null, false, true);
if (!is_array($data)) {
$data = $this->initData(false);
$data = $this->initData();
$file->ftruncate();
$file->serialize($data, false, true);
}
@ -160,14 +167,38 @@ class RunFile {
# 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
* 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(), [
"pgid" => $pgid,
"pid" => null,
"date_start" => new DateTime(),
]);
});
}
/** indiquer que l'application démarre. */
function wfStart(): void {
$this->update(function (array $data) {
return cl::merge($this->initData(), [
"pg_pid" => $data["pg_pid"],
]);
$pid = posix_getpid();
if ($data["pgid"] !== null) {
A::merge($data, [
"pid" => $pid,
]);
} else {
$data = cl::merge($this->initData(), [
"pid" => $pid,
"date_start" => new DateTime(),
]);
}
return $data;
});
}
@ -183,13 +214,12 @@ class RunFile {
return $data["date_start"] !== null && $data["date_stop"] === null;
}
/**
* 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;
function _getCid(array $data=null): int {
if ($data["pgid"] !== null) 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:
@ -208,10 +238,22 @@ class RunFile {
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()];
return [
"date_stop" => new DateTime(),
];
});
}
@ -228,88 +270,75 @@ class RunFile {
}
/** après l'arrêt de l'application, mettre à jour le code de retour */
function wfStopped(int $exitcode): void {
function wfReaped(int $exitcode): void {
$this->update(function (array $data) use ($exitcode) {
return [
"pg_pid" => null,
"pgid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $exitcode,
"is_reaped" => true,
];
});
}
/**
* 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_done"]) {
return false;
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;
}
$done = true;
if ($updateDone) return ["is_done" => $done];
else return null;
});
return $done;
return false;
}
return true;
}
#~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
# 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(),
];
});
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 la tâche devrait tourner mais en réalité
* ce n'est pas le cas
*/
function tm_isUndead(?int $pid=null): bool {
function _isUndead(?int $pid=null): bool {
$data = $this->read();
if ($data["date_start"] === null) return false;
if ($data["date_stop"] !== null) return false;
@ -332,23 +361,104 @@ class RunFile {
return false;
}
function tm_isReapable(): bool {
$data = $this->read();
return $data["date_stop"] !== null && $data["exitcode"] === null;
/**
* 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;
}
/** 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) {
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 [
"pg_pid" => null,
"date_stop" => $data["date_stop"] ?? new DateTime(),
"exitcode" => $data["exitcode"] ?? $exitcode,
"action" => $title,
"action_date_start" => new DateTime(),
"action_max_step" => $maxSteps,
"action_current_step" => 0,
];
});
app::_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,
];
});
app::_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);
}
}

View File

@ -1,8 +1,8 @@
<?php
namespace nur\sery\wip\app;
namespace nur\sery\app;
use Closure;
use nur\sery\A;
use nur\sery\app\cli\Application;
use nur\sery\cl;
use nur\sery\ExitError;
use nur\sery\os\path;
@ -10,10 +10,8 @@ use nur\sery\os\sh;
use nur\sery\php\func;
use nur\sery\str;
use nur\sery\ValueException;
use nur\sery\wip\app\cli\Application;
#XXX une réécriture de app, qui remplacera app à terme
class app2 {
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);

View File

@ -1,5 +1,5 @@
<?php
namespace nur\sery\wip\app;
namespace nur\sery\app;
use nur\sery\A;
use nur\sery\str;

View File

@ -4,11 +4,11 @@
# - NULIB_APP_app_params : paramètres du projet
use nur\sery\os\path;
use nur\sery\wip\app\app2;
use nur\sery\app\app;
if ($argc <= 1) die("invalid arguments");
app2::init(NULIB_APP_app_params);
app::init(NULIB_APP_app_params);
$app = $argv[1];
if (class_exists($app)) {
@ -17,13 +17,13 @@ if (class_exists($app)) {
$app::run();
} elseif (is_executable($app)) {
# la configuration est passée par une variable d'environnement
app2::params_putenv();
app::params_putenv();
pcntl_exec($app, array_slice($argv, 1));
} else {
# la configuration est celle actuellement chargée
array_splice($argv, 0, 1); $argc--;
$name = preg_replace('/\.php$/', "", path::basename($app));
app2::init([
app::init([
"name" => $name,
]);
require $app;

View File

@ -7,12 +7,12 @@ require __DIR__.'/../vendor/autoload.php';
# (par défaut c'est le répertoire bin/) et modifier les paramètres si nécessaire
use nur\sery\tools\BgLauncherApp;
use nur\sery\wip\app\app2;
use nur\sery\app\app;
# chemin vers le lanceur PHP
const NULIB_APP_app_launcher = __DIR__.'/../_cli/_launcher.php';
app2::init([
app::init([
"projdir" => __DIR__ . '/..',
"appcode" => \app\config\bootstrap::APPCODE,
]);

View File

@ -1,137 +0,0 @@
<?php
namespace nur\sery\app;
use nur\sery\cl;
use nur\sery\file\TmpfileWriter;
use nur\sery\os\path;
use nur\sery\os\proc\Cmd;
use nur\sery\output\msg;
use nur\sery\StateException;
use nur\sery\str;
use nur\sery\wip\app\app;
#XXX sera obsolète quand cli\bg_launcher sera mis en prod
class launcher {
/**
* transformer une liste d'argument de la forme
* - ["myArg" => $value] devient ["--my-arg", "$value"]
* - ["myOpt" => true] devient ["--my-opt"]
* - ["myOpt" => false] est momis
* - les valeurs séquentielles sont prises telles quelles
*/
static function verifix_args(array $args): array {
if (!cl::is_list($args)) {
$fixedArgs = [];
$index = 0;
foreach ($args as $arg => $value) {
if ($arg === $index) {
$index++;
$fixedArgs[] = $value;
continue;
} elseif ($value === false) {
continue;
}
$arg = str::us2camel($arg);
$arg = str::camel2us($arg, false, "-");
$arg = str_replace("_", "-", $arg);
$fixedArgs[] = "--$arg";
if ($value !== true) $fixedArgs[] = "$value";
}
$args = $fixedArgs;
}
# corriger le chemin de l'application pour qu'il soit absolu et normalisé
$args[0] = path::abspath($args[0]);
return $args;
}
static function launch(string $appClass, array $args): int {
$app = app::get();
$vendorBindir = $app->getVendorbindir();
$launch_php = "$vendorBindir/_launch.php";
if (!file_exists($launch_php)) {
$launch_php = __DIR__."/../../lib/_launch.php";
}
$tmpfile = new TmpfileWriter();
$tmpfile->keep()->serialize($app->getParams());
$args = self::verifix_args($args);
$cmd = new Cmd([
$launch_php,
"--internal-use", $tmpfile->getFile(),
$appClass, "--", ...$args,
]);
$cmd->addRedir("both", "/tmp/nulib_app_launcher-launch.log");
$cmd->passthru($exitcode);
# attendre un peu que la commande aie le temps de s'initialiser
sleep(1);
$tmpfile->close();
return $exitcode;
}
static function _start(array $args, Runfile $runfile): bool {
if ($runfile->warnIfLocked()) return false;
$pid = pcntl_fork();
if ($pid == -1) {
# parent, impossible de forker
throw new StateException("unable to fork");
} elseif ($pid) {
# parent, fork ok
return true;
} else {
## child, fork ok
# Créer un groupe de process, pour pouvoir tuer tous les enfants en même temps
$runfile->tm_startPg();
$logfile = $runfile->getLogfile() ?? "/tmp/nulib_app_launcher-_start.log";
$pid = posix_getpid();
$exitcode = -776;
try {
# puis lancer la commande
$cmd = new Cmd($args);
$cmd->addSource("/g/init.env");
$cmd->addRedir("both", $logfile, true);
msg::debug("$pid: launching\n".$cmd->getCmd());
$cmd->fork_exec($exitcode);
msg::debug("$pid: exitcode=$exitcode");
return true;
} finally {
$runfile->wfStopped($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->wfStopped(-778);
msg::asuccess();
}
}

View File

@ -4,7 +4,7 @@ namespace nur\sery\os;
use nur\sery\cl;
use nur\sery\ExitError;
use nur\sery\StateException;
use nur\sery\wip\app\app2;
use nur\sery\app\app;
class sh {
static final function _quote(string $value): string {
@ -140,7 +140,7 @@ class sh {
pcntl_waitpid($pid, $status);
if (pcntl_wifexited($status)) $retcode = pcntl_wexitstatus($status);
elseif (pcntl_wifsignaled($status)) $retcode = -pcntl_wtermsig($status);
else $retcode = app2::EC_FORK_CHILD;
else $retcode = app::EC_FORK_CHILD;
return $retcode == 0;
}
@ -154,7 +154,7 @@ class sh {
$pid = pcntl_fork();
if ($pid == -1) {
// parent, impossible de forker
throw new ExitError(app2::EC_FORK_PARENT, "unable to fork");
throw new ExitError(app::EC_FORK_PARENT, "unable to fork");
} elseif ($pid) {
// parent, fork ok
if ($wait) return self::_waitpid($pid, $retcode);

View File

@ -6,9 +6,9 @@ use nur\sery\os\path;
use nur\sery\os\proc\Cmd;
use nur\sery\os\sh;
use nur\sery\output\msg;
use nur\sery\wip\app\app2;
use nur\sery\wip\app\cli\Application;
use nur\sery\wip\app\RunFile;
use nur\sery\app\app;
use nur\sery\app\cli\Application;
use nur\sery\app\RunFile;
use nur\yaml;
class BgLauncherApp extends Application {
@ -58,16 +58,16 @@ class BgLauncherApp extends Application {
self::die("Cette application ne supporte le lancement en tâche de fond");
}
$runfile = app2::with($appClass)->getRunfile();
$runfile = app::with($appClass)->getRunfile();
switch ($this->action) {
case self::ACTION_START:
$appClass::_manage_runfile(count($args), $args, $runfile);
if ($runfile->warnIfLocked()) self::exit(app2::EC_LOCKED);
if ($runfile->warnIfLocked()) self::exit(app::EC_LOCKED);
array_splice($args, 0, 0, [
PHP_BINARY,
path::abspath(NULIB_APP_app_launcher),
]);
app2::params_putenv();
app::params_putenv();
self::_start($args, $runfile);
break;
case self::ACTION_STOP:
@ -84,20 +84,18 @@ class BgLauncherApp extends Application {
$pid = pcntl_fork();
if ($pid == -1) {
# parent, impossible de forker
throw new ExitError(app2::EC_FORK_PARENT, "Unable to fork");
throw new ExitError(app::EC_FORK_PARENT, "Unable to fork");
} elseif (!$pid) {
# child, fork ok
$runfile->wfPrepare($pid);
$outfile = $runfile->getOutfile() ?? "/tmp/NULIB_APP_app_console.out";
$exitcode = app2::EC_FORK_CHILD;
$exitcode = app::EC_FORK_CHILD;
try {
# rediriger STDIN, STDOUT et STDERR
fclose(STDIN);
$in = fopen("/dev/null", "rb");
fclose(STDOUT);
$out = fopen($outfile, "a+b");
fclose(STDERR);
$err = fopen($outfile, "a+b");
fclose(fopen($outfile, "wb")); // vider le fichier
fclose(STDIN); $in = fopen("/dev/null", "rb");
fclose(STDOUT); $out = fopen($outfile, "ab");
fclose(STDERR); $err = fopen($outfile, "ab");
# puis lancer la commande
$cmd = new Cmd($args);
$cmd->addSource("/g/init.env");

View File

@ -4,8 +4,8 @@ namespace nur\sery\tools;
use nur\sery\output\msg;
use nur\sery\php\time\DateTime;
use nur\sery\text\words;
use nur\sery\wip\app\app2;
use nur\sery\wip\app\cli\Application;
use nur\sery\app\app;
use nur\sery\app\cli\Application;
class SteamTrainApp extends Application {
const PROJDIR = __DIR__.'/../..';
@ -33,13 +33,13 @@ EOT,
protected bool $installSignalHandler = true;
function main() {
if ($this->installSignalHandler) app2::install_signal_handler();
if ($this->installSignalHandler) app::install_signal_handler();
$count = intval($this->count);
msg::info("Starting train for ".words::q($count, "step#s"));
app2::action("Running train...", $count);
app::action("Running train...", $count);
for ($i = 1; $i <= $count; $i++) {
msg::print("Tchou-tchou! x $i");
app2::step();
app::step();
sleep(1);
}
msg::info("Stopping train at ".new DateTime());

View File

@ -2,7 +2,7 @@
namespace nur\sery\wip\app {
use nulib\tests\TestCase;
use nur\sery\wip\app\impl\config;
use nur\sery\wip\app\impl\myapp2;
use nur\sery\wip\app\impl\myapp;
use nur\sery\wip\app\impl\MyApplication1;
use nur\sery\wip\app\impl\MyApplication2;
@ -11,8 +11,8 @@ namespace nur\sery\wip\app {
$projdir = config::get_projdir();
$cwd = getcwd();
myapp2::reset();
$app1 = myapp2::with(MyApplication1::class);
myapp::reset();
$app1 = myapp::with(MyApplication1::class);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
@ -30,7 +30,7 @@ namespace nur\sery\wip\app {
"title" => null,
], $app1->getParams());
$app2 = myapp2::with(MyApplication2::class, $app1);
$app2 = myapp::with(MyApplication2::class, $app1);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
@ -53,8 +53,8 @@ namespace nur\sery\wip\app {
$projdir = config::get_projdir();
$cwd = getcwd();
myapp2::reset();
myapp2::init(MyApplication1::class);
myapp::reset();
myapp::init(MyApplication1::class);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
@ -70,9 +70,9 @@ namespace nur\sery\wip\app {
"profile" => "devel",
"name" => "my-application1",
"title" => null,
], myapp2::get()->getParams());
], myapp::get()->getParams());
myapp2::init(MyApplication2::class);
myapp::init(MyApplication2::class);
self::assertSame([
"projdir" => $projdir,
"vendor" => [
@ -88,7 +88,7 @@ namespace nur\sery\wip\app {
"profile" => "devel",
"name" => "my-application2",
"title" => null,
], myapp2::get()->getParams());
], myapp::get()->getParams());
}
}
}
@ -96,7 +96,7 @@ namespace nur\sery\wip\app {
namespace nur\sery\wip\app\impl {
use nur\cli\Application2;
use nur\sery\os\path;
use nur\sery\wip\app\app2;
use nur\sery\app\app;
class config {
const PROJDIR = __DIR__.'/../../..';
@ -106,7 +106,7 @@ namespace nur\sery\wip\app\impl {
}
}
class myapp2 extends app2 {
class myapp extends app {
static function reset(): void {
self::$app = null;
}

View File

@ -3,6 +3,7 @@
namespace nur\sery\wip\app;
use nulib\tests\TestCase;
use nur\sery\app\app;
class appTest extends TestCase {
function testVerifix_name() {

View File

@ -3,6 +3,7 @@
namespace nur\sery\wip\app;
use nulib\tests\TestCase;
use nur\sery\app\args;
class argsTest extends TestCase {
function testFrom_array() {

View File

@ -1,89 +0,0 @@
<?php
namespace nur\sery\wip\app;
use nur\sery\cl;
use nur\sery\file\SharedFile;
use nur\sery\output\msg;
use nur\sery\php\time\DateTime;
/**
* Class LockFile: une classe qui permet à une application de verrouiller
* certaines actions
*/
class LockFile {
const NAME = null;
const TITLE = null;
function __construct($file, ?string $name=null, ?string $title=null) {
$this->file = new SharedFile($file);
$this->name = $name ?? static::NAME;
$this->title = $title ?? static::TITLE;
}
/** @var SharedFile */
protected $file;
/** @var ?string */
protected $name;
/** @var ?string */
protected $title;
protected function initData(): array {
return [
"name" => $this->name,
"title" => $this->title,
"locked" => false,
"date_lock" => null,
"date_release" => null,
];
}
function read(bool $close=true): array {
$data = $this->file->unserialize(null, $close);
if (!is_array($data)) $data = $this->initData();
return $data;
}
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] -- $data[title]");
return true;
}
return false;
}
function lock(?array &$data=null): bool {
$file = $this->file;
$data = $this->read(false);
if ($data["locked"]) {
$file->close();
return false;
} else {
$file->ftruncate();
$file->serialize(cl::merge($data, [
"locked" => true,
"date_lock" => new DateTime(),
"date_release" => null,
]));
return true;
}
}
function release(?array &$data=null): void {
$file = $this->file;
$data = $this->read(false);
$file->ftruncate();
$file->serialize(cl::merge($data, [
"locked" => false,
"date_release" => new DateTime(),
]));
}
}

View File

@ -1,464 +0,0 @@
<?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\os\sh;
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 $outfile=null) {
$file = path::ensure_ext($file, self::RUN_EXT);
$this->name = $name ?? static::NAME;
$this->file = new SharedFile($file);
$this->outfile = $outfile;
}
protected ?string $name;
protected SharedFile $file;
protected ?string $outfile;
function getOutfile(): ?string {
return $this->outfile;
}
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,
"pgid" => null,
"pid" => null,
"serial" => 0,
# lock
"locked" => false,
"date_lock" => null,
"date_release" => null,
# run
"logfile" => $this->outfile,
"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(), [
"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["pgid"] !== null) {
A::merge($data, [
"pid" => $pid,
]);
} else {
$data = cl::merge($this->initData(), [
"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["pgid"] !== null) 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 [
"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 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);
}
}

View File

@ -1,366 +0,0 @@
<?php
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\cl;
use nur\sery\os\path;
use nur\sery\os\sh;
use nur\sery\str;
use nur\sery\ValueException;
class app {
/**
* @var array répertoires vendor exprimés relativement à PROJDIR
*/
const DEFAULT_VENDOR = [
"bindir" => "vendor/bin",
"autoload" => "vendor/autoload.php",
];
private static function isa_Application($app): bool {
if (!is_string($app)) return false;
return $app === Application::class || is_subclass_of($app, Application::class);
}
private static function verifix_name(string &$name): void {
# si $name est une classe, enlever le package et normaliser
# my\package\MyApplication --> my-application
$name = preg_replace('/.*\\\\/', "", $name);
$name = str::without_suffix("-app", str::camel2us($name, false, "-"));
}
/** @param Application|string */
static function with($app, ?array $internal_use_params=null): self {
if ($app instanceof Application) {
$params = [
"projdir" => $app::PROJDIR,
"vendor" => $app::VENDOR,
"appcode" => $app::APPCODE,
"apptype" => "cli",
"name" => $app::NAME,
"title" => $app::TITLE,
"datadir" => $app::DATADIR,
"etcdir" => $app::ETCDIR,
"vardir" => $app::VARDIR,
"logdir" => $app::LOGDIR,
];
} elseif (self::isa_Application($app)) {
$params = [
"projdir" => constant("$app::PROJDIR"),
"vendor" => constant("$app::VENDOR"),
"appcode" => constant("$app::APPCODE"),
"apptype" => "cli",
"name" => constant("$app::NAME"),
"title" => constant("$app::TITLE"),
"datadir" => constant("$app::DATADIR"),
"etcdir" => constant("$app::ETCDIR"),
"vardir" => constant("$app::VARDIR"),
"logdir" => constant("$app::LOGDIR"),
];
} elseif (is_array($app)) {
$params = $app;
} else {
throw ValueException::invalid_type($app, Application::class);
}
if ($internal_use_params !== null) {
$params = array_merge($internal_use_params, cl::selectm($params, [
"name",
"title",
], [
"apptype" => "cli",
]));
self::verifix_name($params["name"]);
}
return new static($params, $internal_use_params !== null);
}
protected static ?app $app = null;
static function init($app, ?array $internal_use_params=null): void {
self::$app = static::with($app, $internal_use_params);
}
static function get(): self {
return self::$app ??= new self(null);
}
function __construct(?array $params, bool $internalUse_asis=false) {
if ($internalUse_asis) {
[
"projdir" => $this->projdir,
"vendor" => $this->vendor,
"appcode" => $this->appcode,
"apptype" => $this->apptype,
"name" => $this->name,
"title" => $this->title,
"profile" => $this->profile,
"cwd" => $this->cwd,
"datadir" => $this->datadir,
"etcdir" => $this->etcdir,
"vardir" => $this->vardir,
"logdir" => $this->logdir,
] = $params;
} else {
$this->projdir = $projdir = path::abspath($params["projdir"] ?? ".");
$vendor = $params["vendor"] ?? self::DEFAULT_VENDOR;
$vendor["bindir"] = path::reljoin($projdir, $vendor["bindir"]);
$vendor["autoload"] = path::reljoin($projdir, $vendor["autoload"]);
$this->vendor = $vendor;
$this->appcode = $appcode = $params["appcode"] ?? "app";
$this->apptype = $apptype = $params["apptype"] ?? "cli";
$name = $params["name"] ?? null;
if ($name === null) {
$name = $appcode;
} else {
# si $name est une classe, enlever le package et normaliser
$name = preg_replace('/.*\\\\/', "", $name);
$name = str::without_suffix("-app", str::camel2us($name, false, "-"));
}
$this->name = $name;
$this->title = $params["title"] ?? null;
$appcode = str_replace("-", "_", strtoupper($appcode));
# profile
$profile = getenv("${appcode}_PROFILE");
if ($profile === false) $profile = getenv("APP_PROFILE");
if ($profile === false) $profile = $params["profile"] ?? null;
if ($profile === null) {
if (file_exists("$projdir/.default-profile-devel")) $profile = "devel";
else $profile = "prod";
}
$this->profile = $profile;
# cwd
$this->cwd = getcwd();
# datadir
$datadir = getenv("${appcode}_DATADIR");
if ($datadir === false) $datadir = $params["datadir"] ?? null;
if ($datadir === null) $datadir = "devel/$apptype";
$this->datadir = $datadir = path::reljoin($projdir, $datadir);
# etcdir
$etcdir = getenv("${appcode}_ETCDIR");
if ($etcdir === false) $etcdir = $params["etcdir"] ?? null;
if ($etcdir === null) $etcdir = "etc";
$this->etcdir = $etcdir = path::reljoin($datadir, $etcdir);
# vardir
$vardir = getenv("${appcode}_VARDIR");
if ($vardir === false) $vardir = $params["vardir"] ?? null;
if ($vardir === null) $vardir = "var";
$this->vardir = $vardir = path::reljoin($datadir, $vardir);
# logdir
$logdir = getenv("${appcode}_LOGDIR");
if ($logdir === false) $logdir = $params["logdir"] ?? null;
if ($logdir === null) $logdir = "log";
$this->logdir = $logdir = path::reljoin($datadir, $logdir);
}
}
/** recréer le tableau des paramètres */
function getParams(): array {
return [
"projdir" => $this->projdir,
"vendor" => $this->vendor,
"appcode" => $this->appcode,
"apptype" => $this->apptype,
"name" => $this->name,
"title" => $this->title,
"profile" => $this->profile,
"cwd" => $this->cwd,
"datadir" => $this->datadir,
"etcdir" => $this->etcdir,
"vardir" => $this->vardir,
"logdir" => $this->logdir,
];
}
protected string $projdir;
function getProjdir(): string {
return $this->projdir;
}
protected array $vendor;
function getVendorBindir(): string {
return $this->vendor["bindir"];
}
function getVendorAutoload(): string {
return $this->vendor["autoload"];
}
protected string $appcode;
function getAppcode(): string {
return $this->appcode;
}
protected string $apptype;
function getApptype(): string {
return $this->apptype;
}
protected string $name;
function getName(): ?string {
return $this->name;
}
protected ?string $title;
function getTitle(): ?string {
return $this->title;
}
protected string $profile;
function getProfile(): string {
return $this->profile;
}
/**
* @param ?string|false $profile
*/
function withProfile(string $file, $profile): string {
if ($profile !== false) {
if ($profile === null) $profile = $this->getProfile();
[$dir, $filename] = path::split($file);
$basename = path::basename($filename);
$ext = path::ext($file);
$file = path::join($dir, "$basename.$profile$ext");
}
return $file;
}
function findFile(array $dirs, array $names, $profile=null): string {
# d'abord chercher avec le profil
if ($profile !== false) {
foreach ($dirs as $dir) {
foreach ($names as $name) {
$file = path::join($dir, $name);
$file = $this->withProfile($file, $profile);
if (file_exists($file)) return $file;
}
}
}
# puis sans profil
foreach ($dirs as $dir) {
foreach ($names as $name) {
$file = path::join($dir, $name);
if (file_exists($file)) return $file;
}
}
# la valeur par défaut est avec profil
return $this->withProfile(path::join($dirs[0], $names[0]), $profile);
}
function fencedJoin(string $basedir, string $path): string {
$path = path::reljoin($basedir, $path);
if (!path::is_within($path, $basedir)) {
throw ValueException::invalid_value($path, "path");
}
return $path;
}
protected string $cwd;
function getCwd(): string {
return $this->cwd;
}
protected string $datadir;
function getDatadir(): string {
return $this->datadir;
}
protected string $etcdir;
function getEtcdir(): string {
return $this->etcdir;
}
function getEtcfile(string $name, $profile=null): string {
return $this->findFile([$this->etcdir], [$name], $profile);
}
protected string $vardir;
function getVardir(): string {
return $this->vardir;
}
function getVarfile(string $name, $profile=null): string {
$file = $this->withProfile($this->fencedJoin($this->vardir, $name), $profile);
sh::mkdirof($file);
return $file;
}
protected string $logdir;
function getLogdir(): string {
return $this->logdir;
}
function getLogfile(?string $name=null, $profile=null): string {
if ($name === null) $name = "{$this->name}.log";
$file = $this->withProfile($this->fencedJoin($this->logdir, $name), $profile);
sh::mkdirof($file);
return $file;
}
/**
* obtenir le chemin absolu vers un fichier de travail
* - si le chemin est absolu, il est inchangé
* - si le chemin est qualifié (commence par ./ ou ../) ou sans chemin, il est
* exprimé par rapport à $vardir
* - sinon le chemin est exprimé par rapport au répertoire de travail de base
* $datadir
*
* is $ensure_dir, créer le répertoire du fichier s'il n'existe pas déjà
*/
function getWorkfile(?string $file, $profile=null, bool $ensureDir=true): ?string {
if ($file === null) return null;
if (path::is_qualified($file) || !path::have_dir($file)) {
$file = path::reljoin($this->vardir, $file);
} else {
$file = path::reljoin($this->datadir, $file);
}
$file = $this->withProfile($file, $profile);
if ($ensureDir) sh::mkdirof($file);
return $file;
}
/**
* obtenir le chemin absolu vers un fichier spécifié par l'utilisateur.
* - si le chemin commence par /, il est laissé en l'état
* - si le chemin commence par ./ ou ../, il est exprimé par rapport à $cwd
* - sinon le chemin est exprimé par rapport au répertoire de travail $vardir
*/
function getUserfile(?string $file): ?string {
if ($file === null) return null;
if (path::is_qualified($file)) {
return path::reljoin($this->cwd, $file);
} else {
return path::reljoin($this->vardir, $file);
}
}
protected ?RunFile $runfile = null;
function getRunfile(): RunFile {
$name = $this->name;
$runfile = $this->getWorkfile($name);
$logfile = $this->getLogfile();
return $this->runfile ??= new RunFile($name, $runfile, $logfile);
}
protected ?array $lockFiles = null;
function getLockfile(?string $name=null): LockFile {
$this->lockFiles[$name] ??= $this->getRunfile()->getLockFile($name, $this->title);
return $this->lockFiles[$name];
}
}

View File

@ -1,576 +0,0 @@
<?php
namespace nur\sery\wip\os\proc;
use nur\sery\php\coll\AutoArray;
/**
* Class ManagedTask: une tâche de fond
*
* --autogen-properties-and-methods--
*/
class ManagedTask extends AutoArray {
const LOCK = "task";
const DEFINITION_SCHEMA = [
"id" => ["string", null, "identifiant de la tâche"],
"serial" => ["int", null, "numéro de série permettant de distinguer deux occurrences de la tâche"],
"title" => ["?string", null, "description de la tâche"],
"valid" => ["bool", false, "la tâche est-elle valide?"],
"owner_login" => ["?string", null, "compte de la personne qui a lancé la tâche"],
"owner_name" => ["?string", null, "nom de la personne qui a lancé la tâche"],
"owner_page" => ["?string", null, "page qui a créé cette tâche"],
"owner_params" => ["?array", null, "paramètres à passer à la page"],
"cmd" => [null, null, "commande à lancer"],
"logfile" => ["?string", null, "sortie de la commande"],
];
const SCHEMA = [
"definition" => [
"?array", null, "définition de la tâche",
"schema" => self::DEFINITION_SCHEMA,
],
"state" => [
"?array", null, "instance de la tâche",
"schema" => [
"definition" => [
"array", null, "copie de la définition de la tâche",
"schema" => self::DEFINITION_SCHEMA,
],
"started" => ["bool", false, "la tâche a-t-elle été démarrée?"],
"date_start" => ["?datetime", null, "date du démarrage de la tâche"],
"pid" => ["?int", null, "PID du process contrôleur"],
"status" => ["?string", null, "Message de statut indiqué par la tâche"],
"stopped" => ["bool", false, "la tâche est-elle terminée?"],
"date_stop" => ["?datetime", null, "date de l'arrêt de la tâche"],
"retcode" => ["?int", null, "code de retour de la commande"],
"done" => ["bool", false, "la fin de la tâche a-t-elle été prise en compte?"],
],
],
"" => [
"auto_properties" => [
"id" => "definition.id",
"serial" => "definition.serial",
"title" => "definition.title",
"valid" => "definition.valid",
"owner_login" => "definition.owner_login",
"owner_name" => "definition.owner_name",
"owner_page" => "definition.owner_page",
"owner_params" => "definition.owner_params",
"cmd" => "definition.cmd",
"logfile" => "definition.logfile",
]
],
];
const _AUTO_PROPERTIES = self::SCHEMA[""]["auto_properties"];
const _SCHEMA = [
"id" => ["string", null, "identifiant de la tâche"],
"serial" => ["string", null, "numéro de série permettant de distinguer deux occurrences de la tâche"],
"title" => ["?string", null, "description de la tâche"],
"valid" => ["bool", false, "la tâche est-elle valide?"],
"owner_login" => ["?string", null, "compte de la personne qui a lancé la tâche"],
"owner_name" => ["?string", null, "nom de la personne qui a lancé la tâche"],
"page" => ["?array", null, "page qui a créé cette tâche et paramètres à passer à la page"],
"cmd" => [null, null, "commande à lancer"],
"logfile" => ["?string", null, "sortie de commande"],
"started" => ["bool", false, "la tâche a-t-elle été démarrée?"],
"date_start" => ["?datetime", null, "date du démarrage de la tâche"],
"pid" => ["?int", null, "PID du process contrôleur"],
"stopped" => ["bool", false, "la tâche est-elle terminée?"],
"date_stop" => ["?datetime", null, "date de l'arrêt de la tâche"],
"retcode" => ["?int", null, "code de retour de la commande"],
"done" => ["bool", false, "la fin de la tâche a-t-elle été prise en compte?"],
];
function __construct(string $id, bool $autoUpdate=false, ?callable $init=null) {
# ne pas appeler parent::__construct()
if (file_exists($id)) {
$file = $id;
} else {
$authz = authz::get();
$this->data = [
"id" => $id,
"serial" => 0,
"owner_login" => $authz->getUsername(),
"owner_name" => $authz->getDisplayName(),
];
$file = tasks::pf("$id.task");
}
$this->init = $init;
$this->file = $file;
$this->ensureTask($autoUpdate);
}
/** @var ?callable */
protected $init;
/** @var string */
protected $file;
private function ensureTask(bool $autoUpdate): void {
lock::exlusive(self::LOCK);
try {
if (is_file($this->file)) {
$this->_reload();
} else {
$this->_reset();
$this->_save();
}
$logfile = $this->getLogfile();
if ($logfile !== null) os::mkdirof($logfile);
} finally {
lock::release(self::LOCK);
}
if ($autoUpdate) $this->update();
}
private function _init(): bool {
$init = $this->init;
if ($init !== null) {
func::call($init, $this);
return true;
}
return false;
}
function init(): void {
if ($this->init !== null) {
lock::exlusive(self::LOCK);
try {
$this->_init();
$this->_save();
} finally {
lock::release(self::LOCK);
}
}
}
private function _reset(): void {
$authz = authz::get();
$id = $this->data["id"];
$serial = A::get($this->data, "serial", 0);
$this->data = $this->ensureData([
"id" => $id,
"serial" => $serial + 1,
"owner_login" => $authz->getUsername(),
"owner_name" => $authz->getDisplayName(),
"logfile" => logs::pf("$id/latest.log"),
]);
$this->_init();
}
function reset(): void {
lock::exlusive(self::LOCK);
try {
$this->_reset();
$this->_save();
} finally {
lock::release(self::LOCK);
}
}
private function _reload(): void {
$this->data = unserialize(file_get_contents($this->file));
}
function reload(): void {
lock::exlusive(self::LOCK);
try {
$this->_reload();
} finally {
lock::release(self::LOCK);
}
}
private function _save(): void {
os::mkdirof($this->file);
$outf = fopen($this->file, "w+");
fwrite($outf, serialize($this->data));
fclose($outf);
}
function save(): void {
lock::exlusive(self::LOCK);
try {
$this->_save();
} finally {
lock::release(self::LOCK);
}
}
/** vérifier que l'objet est bien initialisé */
function validate(): void {
if (!$this->isValid()) {
if ($this->getCmd() === null) {
throw new ValueException("cmd is required");
}
$this->setValid(true);
$this->save();
}
}
function _launch(): void {
$args = [
__DIR__.'/../../lib/launch_task.php',
"--envname", envs::get(),
$this->getId(),
];
$logfile = $this->getLogfile();
if ($logfile !== null) A::merge($args, ["--logfile", $logfile]);
$cmd = new Cmd($args);
$cmd->addRedir("null");
$cmd->passthru();
$this->reload();
}
function isLaunchable(): bool {
return $this->isStarted();
}
function launch(): void {
if (!$this->isStartable()) return;
if ($this->isDone()) $this->reset();
$this->_launch();
}
function isUpdatable(): bool {
return $this->isLaunchable() && !$this->isDone();
}
function update(): void {
if ($this->isUpdatable()) $this->_launch();
else $this->init();
}
function kill(): void {
if (!$this->isStarted() || $this->isStopped()) return;
$args = [
__DIR__.'/../../lib/launch_task.php',
"-e", envs::get(),
"--kill",
$this->getId(),
];
$logfile = $this->getLogfile();
if ($logfile !== null) A::merge($args, ["-L", $logfile]);
$cmd = new Cmd($args);
$cmd->addRedir("null");
$cmd->passthru();
$this->reload();
}
function isStartable(): bool {
return !$this->isStarted() || $this->isDone();
}
/**
* démarrer la commande. doit être lancé depuis launch_task.php
*/
function ltStart(?string $logfile): void {
$pid = pcntl_fork();
if ($pid == -1) {
# parent, impossible de forker
throw new IllegalAccessException("unable to fork");
} elseif ($pid) {
# parent, fork ok
$this->setStarted(true);
$this->setDateStart(date::datetime());
$this->setPid($pid);
$this->save();
} else {
## child, fork ok
# Créer un groupe de process, pour pouvoir les tuer toutes en même temps
posix_setsid();
msg::push($oldMsg, null, [
"output" => $logfile,
]);
$retcode = -776;
try {
# tout d'abord synchroniser les fichiers le cas échéant
$command = $this->get("command");
$append = false;
if ($command !== null) {
$files = $command["files"];
$forceSync = $this->get("force_sync");
files::sync($files, $forceSync, $logfile, "wb");
$append = true;
}
# puis lancer la commande
$cmd = Cmd::with($this->getCmd());
if ($logfile !== null) $cmd->addRedir("both", $logfile, $append);
$cmd->fork_exec($retcode);
} catch (Exception $e) {
msg::error($e);
} finally {
$this->reload();
$this->setStopped(true);
$this->setDateStop(date::datetime());
$this->setRetcode($retcode);
$this->save();
msg::pop($oldMsg);
}
}
}
/** arrêter la commande. doit être lancé depuis launch_task.php */
function ltKill(?string $logfile): void {
msg::push($oldMsg, null, $logfile);
try {
$id = $this->getId();
$pid = $this->getPid();
msg::action("$id: $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 ($this->ltIsUndead()) {
sleep(1);
if (--$timeout == 0) {
msg::afailure("tentative d'arrêt de la tâche");
return;
}
}
msg::asuccess("tâche arrêtée");
$this->setStopped(true);
$this->setDateStop(date::datetime());
$this->setRetcode(-787);
$this->setDone(true);
$this->save();
} finally {
msg::pop($oldMsg);
}
}
function isReapable(): bool {
return $this->isStopped() && !$this->isDone();
}
/**
* marquer la commande comme terminée. doit être lancé depuis launch_task.php
*/
function ltReap(): void {
pcntl_waitpid($this->getPid(), $status);
$this->setDone(true);
$this->save();
}
/**
* vérifier si on est dans le cas la tâche est censée tourner mais en
* réalité ce n'est pas le cas. doit être lancé depuis launch_task.php
*/
function ltIsUndead(): bool {
if (!posix_kill($this->getPid(), 0)) {
switch (posix_get_last_error()) {
case PCNTL_ESRCH:
# process inexistant
return true;
case PCNTL_EPERM:
# process auquel on n'a pas accès: ce doit être un autre process qui a
# réutilisé le PID
return true;
case PCNTL_EINVAL:
# ne devrait pas se produire
return false;
}
}
# process existant
return false;
}
/**
* marquer la tâche comme terminée avec un code d'erreur si elle n'existe
* plus. doit être lancé depuis launch_task.php
*/
function ltCleanUndead(): void {
if (!$this->isStopped()) {
$this->setStopped(true);
$this->setDateStop(date::datetime());
$this->setRetcode(-777);
}
$this->setDone(true);
$this->save();
}
function getIdTitle(): string {
$idTitle = $this->getId();
$title = $this->getTitle();
if ($title) $idTitle .= " -- $title";
return $idTitle;
}
function getNameOrLogin(): string {
$nameOrLogin = $this->getOwnerName();
if ($nameOrLogin === null) $nameOrLogin = $this->getOwnerLogin();
if ($nameOrLogin === null) $nameOrLogin = "(unknown)";
return $nameOrLogin;
}
const MAX_LOG_SIZE = 256 * 1024;
const CACTION_NONE = "n";
const CACTION_REPLACE = "r";
const CACTION_UPDATE = "u";
function export(?int $serial=null, ?int $cs=null, ?int $ce=null): array {
$task = $this->array();
$dateStart = new Datetime($this->getDateStart());
$dateStop = new Datetime($this->getDateStop());
# $ca = action à faire par le client: replace ou update
# ls = local start, le = local end (local === server en l'occurrence)
# cs = client start, ce = client end (pour CACTION_REPLACE)
# $ps = plus start, $pe = plus end (pour CACTION_UPDATE)
# $rs = read start, $re = read end
if ($serial !== null && $cs !== null && $ce !== null && $this->isStarted()) {
lock::exlusive(self::LOCK);
$inf = false;
try {
$logfile = $this->getLogfile();
if (!file_exists($logfile)) {
# s'assurer que le fichier existe (il peut avoir été nettoyé entre temps)
f::close(f::open($logfile, "cb"));
}
$inf = f::open($logfile, "rb");
$le = f::seek($inf, 0, SEEK_END);
$ls = $le - self::MAX_LOG_SIZE;
if ($ls <= 0) {
$ls = 0;
} else {
# trouver le premier saut de ligne
$ls = f::find_nl($inf, $ls);
}
if ($serial != $this->getSerial()) {
# nouvelle tâche, on recommence tout
$rs = $cs = $ls;
$re = $ce = $le;
$ca = self::CACTION_REPLACE;
} elseif ($ls <= $cs) {
# cas courant, on rajoute du contenu, mais pas plus que MAX_LOG_SIZE
$ls = $cs;
$ps = 0;
$pe = $le - $ce;
$rs = $ce;
$re = $le;
$ca = self::CACTION_UPDATE;
$cs = $ps;
$ce = $pe;
} elseif ($ls <= $ce) {
# on a dépassé MAX_LOG_SIZE, il faut recalculer
# garder une partie des logs précédents
$ps = $ls - $cs;
$pe = $le - $ce;
$rs = $ce;
$re = $le;
$ca = self::CACTION_UPDATE;
$cs = $ps;
$ce = $pe;
} else {
# ne rien garder des logs précédents
$rs = $cs = $ls;
$re = $ce = $le;
$ca = self::CACTION_REPLACE;
}
$logSize = $re - $rs;
if ($logSize > 0) {
f::seek($inf, $rs, SEEK_SET);
$log = f::read($inf, $logSize);
$lf = new BaseF(); #XXX
$lf->formatContent($log);
} elseif ($ca == self::CACTION_REPLACE) {
$log = "";
} else {
$log = false;
}
} finally {
if ($inf) f::close($inf);
lock::release(self::LOCK);
}
} else {
$cs = $ce = false;
$ca = self::CACTION_NONE;
$log = false;
}
$page = $this->getPage();
if ($page !== null) {
$dest = A::get($page, 0);
$params = A::get($page, 1);
$pageUrl = page::bu($dest, $params);
} else {
$pageUrl = false;
}
A::merge($task, [
"id_title" => $this->getIdTitle(),
"name_or_login" => $this->getNameOrLogin(),
"page_url" => $pageUrl,
"elapsed_start" => $dateStart->getElapsed()->formatAt(),
"elapsed_stop" => $dateStop->getElapsed()->formatSince(),
"elapsed_total" => $dateStart->getElapsed($dateStop)->formatDelay(),
"launchable" => $this->isLaunchable(),
"updatable" => $this->isUpdatable(),
"startable" => $this->isStartable(),
"reapable" => $this->isReapable(),
"working" => $this->isStarted() && !$this->isDone(),
"ok" => $this->isDone() && $this->getRetcode() == 0,
"ko" => $this->isDone() && $this->getRetcode() != 0,
"log" => $log,
]);
return [$task, $ca, $cs, $ce];
}
#############################################################################
const _AUTOGEN_CONSTS = [
"_AUTO_GETTERS" => [Autogen::class, "auto_getters", self::SCHEMA],
"_AUTO_SETTERS" => [Autogen::class, "auto_setters", self::SCHEMA],
];
const _AUTOGEN_METHODS = [
[Autogen::class, "auto_getters_methods", self::SCHEMA],
[Autogen::class, "auto_setters_methods", self::SCHEMA],
];
const _AUTO_GETTERS = /*autogen*/[
'getId' => 'id',
'getSerial' => 'serial',
'getTitle' => 'title',
'isValid' => 'valid',
'getOwnerLogin' => 'owner_login',
'getOwnerName' => 'owner_name',
'getPage' => 'page',
'getCmd' => 'cmd',
'getLogfile' => 'logfile',
'isStarted' => 'started',
'getDateStart' => 'date_start',
'getPid' => 'pid',
'isStopped' => 'stopped',
'getDateStop' => 'date_stop',
'getRetcode' => 'retcode',
'isDone' => 'done',
];
const _AUTO_SETTERS = /*autogen*/[
'setId' => 'id',
'setSerial' => 'serial',
'setTitle' => 'title',
'setValid' => 'valid',
'setOwnerLogin' => 'owner_login',
'setOwnerName' => 'owner_name',
'setPage' => 'page',
'setCmd' => 'cmd',
'setLogfile' => 'logfile',
'setStarted' => 'started',
'setDateStart' => 'date_start',
'setPid' => 'pid',
'setStopped' => 'stopped',
'setDateStop' => 'date_stop',
'setRetcode' => 'retcode',
'setDone' => 'done',
];
}

View File

@ -1,75 +0,0 @@
<?php
namespace nur\sery\wip\os\proc;
class tasks {
static function pf(string $name): string {
$envname = envs::get();
return "/tasks/$envname/$name";
}
/** le verrou doit être posé avant l'appel de cette méthode */
private static function _list(bool $include_invalids, bool $sort): array {
$tmpfiles = glob(self::pf("*.task"));
if ($tmpfiles === false) return [];
$tasks = [];
foreach ($tmpfiles as $taskfile) {
$task = new ManagedTask($taskfile);
if (!$include_invalids && !$task->isValid()) {
continue;
}
$tasks[] = [$taskfile, $task];
}
if ($sort) {
clearstatcache();
usort($tasks, function ($fta, $ftb) {
/**
* @var ManagedTask $ta
* @var ManagedTask $tb
*/
[$fa, $ta] = $fta;
[$fb, $tb] = $ftb;
# comparer l'état "running"
$wa = $ta->isStarted() && !$ta->isDone();
$wb = $tb->isStarted() && !$tb->isDone();
$c = -base::compare($wa, $wb);
if ($c != 0) return $c;
# comparer la date de dernière modification du fichier
$mta = filemtime($fa);
$mtb = filemtime($fb);
return -base::compare($mta, $mtb);
});
}
return $tasks;
}
/** supprimer toutes les tâches */
static function delete_all(): void {
lock::exlusive(ManagedTask::LOCK);
try {
foreach (self::_list(true, false) as [$taskfile, $task]) {
unlink($taskfile);
}
} finally {
lock::release(ManagedTask::LOCK);
}
}
/**
* retourner la liste des tâches valides
* @return ManagedTask[]
*/
static function list(?string $selectId=null): array {
$tasks = [];
lock::exlusive(ManagedTask::LOCK);
try {
foreach (self::_list(false, true) as [$taskfile, $task]) {
$id = $task->getId();
if ($selectId !== null && $id !== $selectId) continue;
$tasks[$id] = $task;
}
} finally {
lock::release(ManagedTask::LOCK);
}
return $tasks;
}
}

View File

@ -1,19 +1,18 @@
<?php
namespace nur\sery\wip\app\cli;
namespace nur\sery\app\cli;
use Exception;
use nur\cli\ArgsException;
use nur\cli\ArgsParser;
use nur\config;
use nur\sery\app\app;
use nur\sery\app\RunFile;
use nur\sery\ExitError;
use nur\sery\output\console;
use nur\sery\output\log;
use nur\sery\output\msg;
use nur\sery\output\std\StdMessenger;
use nur\sery\ValueException;
use nur\sery\wip\app\app2;
use nur\sery\wip\app\RunFile;
use nur\sery\wip\web\content\v;
use nur\yaml;
/**
@ -145,7 +144,7 @@ EOT);
else $ec = self::_error("not running");
break;
default:
$ec = self::_error("$argv[1]: unexpected command", app2::EC_BAD_COMMAND);
$ec = self::_error("$argv[1]: unexpected command", app::EC_BAD_COMMAND);
}
exit($ec);
}
@ -155,26 +154,26 @@ EOT);
$stop = false;
$shutdown = function () use (&$unlock, &$stop) {
if ($unlock) {
app2::get()->getRunfile()->release();
app::get()->getRunfile()->release();
$unlock = false;
}
if ($stop) {
app2::get()->getRunfile()->wfStop();
app::get()->getRunfile()->wfStop();
$stop = false;
}
};
register_shutdown_function($shutdown);
app2::install_signal_handler(static::INSTALL_SIGNAL_HANDLER);
app::install_signal_handler(static::INSTALL_SIGNAL_HANDLER);
try {
static::_initialize_app();
$useRunfile = static::USE_RUNFILE;
$useRunlock = static::USE_RUNLOCK;
if ($useRunfile) {
$runfile = app2::get()->getRunfile();
$runfile = app::get()->getRunfile();
global $argc, $argv;
self::_manage_runfile($argc, $argv, $runfile);
if ($useRunlock && $runfile->warnIfLocked()) exit(app2::EC_LOCKED);
if ($useRunlock && $runfile->warnIfLocked()) exit(app::EC_LOCKED);
$runfile->wfStart();
$stop = true;
@ -191,12 +190,12 @@ EOT);
exit($e->getCode());
} catch (Exception $e) {
msg::error($e);
exit(app2::EC_UNEXPECTED);
exit(app::EC_UNEXPECTED);
}
}
protected static function _initialize_app(): void {
app2::init(static::class);
app::init(static::class);
msg::set_messenger(new StdMessenger([
"min_level" => msg::DEBUG,
]));
@ -211,7 +210,7 @@ EOT);
]);
if (static::USE_LOGFILE) {
$msgs["log"] = new StdMessenger([
"output" => app2::get()->getLogfile(),
"output" => app::get()->getLogfile(),
"min_level" => msg::MINOR,
"add_date" => true,
]);
@ -254,12 +253,12 @@ EOT);
["group",
["-p", "--profile", "--app-profile",
"args" => 1, "argsdesc" => "PROFILE",
"action" => [app2::class, "set_profile"],
"action" => [app::class, "set_profile"],
"help" => "spécifier le profil d'exécution",
],
["-P", "--prod", "action" => [app2::class, "set_profile", config::PROD]],
["-T", "--test", "action" => [app2::class, "set_profile", config::TEST]],
["--devel", "action" => [app2::class, "set_profile", config::DEVEL]],
["-P", "--prod", "action" => [app::class, "set_profile", config::PROD]],
["-T", "--test", "action" => [app::class, "set_profile", config::TEST]],
["--devel", "action" => [app::class, "set_profile", config::DEVEL]],
],
];
@ -364,7 +363,7 @@ EOT);
/** retourner le profil courant en couleur */
static function get_profile(?string $profile=null): string {
if ($profile === null) $profile = app2::get_profile();
if ($profile === null) $profile = app::get_profile();
foreach (static::PROFILE_COLORS as $text => $color) {
if (strpos($profile, $text) !== false) {
return $color? "<color $color>$profile</color>": $profile;