Module tasks + knowledgecenter_update aus Kundenprojekt übernehmen (bereinigt)
Beide Module wurden aus einem anderen Kundenprojekt kopiert und bereinigt: Knowledgecenter: - Bild-Cache (1.985 Dateien) geleert, Ordnerstruktur mit .gitkeep behalten - Files-Gallery-Lizenzschlüssel entfernt (config.php) - Kundenspezifische Kategorien gelöscht: Teil I/II/III QM-Struktur, nummerierte QM-Kapitel, Gremien (Präsidium/Finanzausschuss/Landesausschuss), Sitzungsservice-Serie, Protokoll-Abteilungen, Testeinträge - 56 generische Kategorien bleiben (Downloads, Onboarding, Personalthemen …) Tasks: - Alle Betriebsdaten gelöscht (114 Tasks, 914 Submissions, 579 Assignments) - Task-Definitionen und Verlinkungen geleert - Kategoriestruktur bleibt (Allgemein, IT, Personal, Buchhaltung …) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
259
module/tasks/cli/CronRuntime.php
Normal file
259
module/tasks/cli/CronRuntime.php
Normal file
@@ -0,0 +1,259 @@
|
||||
<?php
|
||||
declare(strict_types=1);
|
||||
|
||||
final class TaskCertCronRuntime
|
||||
{
|
||||
public static function parseOptions(array $argv, bool $allowTaskId = true): array
|
||||
{
|
||||
$options = [
|
||||
'apply' => false,
|
||||
'dry_run' => false,
|
||||
'json' => false,
|
||||
'no_lock' => false,
|
||||
'task_id' => null,
|
||||
'help' => false,
|
||||
];
|
||||
|
||||
$count = count($argv);
|
||||
for ($i = 1; $i < $count; $i++) {
|
||||
$arg = (string) $argv[$i];
|
||||
|
||||
if ($arg === '--apply') {
|
||||
$options['apply'] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg === '--dry-run') {
|
||||
$options['dry_run'] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg === '--json') {
|
||||
$options['json'] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg === '--no-lock') {
|
||||
$options['no_lock'] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg === '--help') {
|
||||
$options['help'] = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (strpos($arg, '--task_id=') === 0) {
|
||||
if (!$allowTaskId) {
|
||||
throw new InvalidArgumentException('--task_id wird für diesen Job nicht unterstützt.');
|
||||
}
|
||||
$raw = trim(substr($arg, strlen('--task_id=')));
|
||||
if ($raw === '' || !ctype_digit($raw)) {
|
||||
throw new InvalidArgumentException('Ungültiger Wert für --task_id.');
|
||||
}
|
||||
$options['task_id'] = (int) $raw;
|
||||
continue;
|
||||
}
|
||||
|
||||
if ($arg === '--task_id') {
|
||||
if (!$allowTaskId) {
|
||||
throw new InvalidArgumentException('--task_id wird für diesen Job nicht unterstützt.');
|
||||
}
|
||||
$next = $argv[$i + 1] ?? null;
|
||||
if (!is_string($next) || trim($next) === '' || !ctype_digit(trim($next))) {
|
||||
throw new InvalidArgumentException('Ungültiger Wert für --task_id.');
|
||||
}
|
||||
$options['task_id'] = (int) trim($next);
|
||||
$i++;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw new InvalidArgumentException('Unbekannte Option: ' . $arg);
|
||||
}
|
||||
|
||||
if ($options['apply'] && $options['dry_run']) {
|
||||
throw new InvalidArgumentException('--apply und --dry-run können nicht gleichzeitig gesetzt werden.');
|
||||
}
|
||||
|
||||
if ($options['task_id'] !== null && (int) $options['task_id'] <= 0) {
|
||||
throw new InvalidArgumentException('--task_id muss > 0 sein.');
|
||||
}
|
||||
|
||||
return $options;
|
||||
}
|
||||
|
||||
public static function runWithHandling(string $jobName, array $options, callable $runner): int
|
||||
{
|
||||
$startedAt = microtime(true);
|
||||
$runId = self::buildRunId($jobName);
|
||||
$mode = $options['apply'] ? 'apply' : 'dry_run';
|
||||
$lockHandle = null;
|
||||
|
||||
$basePayload = [
|
||||
'timestamp' => date('c'),
|
||||
'job' => $jobName,
|
||||
'job_run_id' => $runId,
|
||||
'mode' => $mode,
|
||||
'task_id' => $options['task_id'],
|
||||
];
|
||||
|
||||
try {
|
||||
if (!$options['no_lock']) {
|
||||
$lockResult = self::acquireLock($jobName);
|
||||
$lockHandle = $lockResult['handle'] ?? null;
|
||||
$acquired = (bool) ($lockResult['acquired'] ?? false);
|
||||
if (!$acquired) {
|
||||
$payload = $basePayload + [
|
||||
'success' => false,
|
||||
'skipped' => true,
|
||||
'skip_reason' => 'lock_active',
|
||||
'duration_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
||||
];
|
||||
self::writeCronLog($payload);
|
||||
if ((bool) ($options['json'] ?? false)) {
|
||||
self::emitJson($payload);
|
||||
} else {
|
||||
fwrite(STDOUT, '[SKIP] ' . $jobName . ': Lock aktiv, Job wird übersprungen.' . PHP_EOL);
|
||||
}
|
||||
return 3;
|
||||
}
|
||||
}
|
||||
|
||||
$context = [
|
||||
'job_run_id' => $runId,
|
||||
'mode' => $mode,
|
||||
'task_id' => $options['task_id'],
|
||||
];
|
||||
|
||||
$summary = $runner($context);
|
||||
if (!is_array($summary)) {
|
||||
$summary = [];
|
||||
}
|
||||
|
||||
$payload = $basePayload + [
|
||||
'success' => true,
|
||||
'duration_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
||||
'summary' => $summary,
|
||||
];
|
||||
self::writeCronLog($payload);
|
||||
if ((bool) ($options['json'] ?? false)) {
|
||||
self::emitJson($payload);
|
||||
} else {
|
||||
fwrite(STDOUT, '[OK] ' . $jobName . ' (' . $mode . ') abgeschlossen in ' . $payload['duration_ms'] . 'ms.' . PHP_EOL);
|
||||
}
|
||||
return 0;
|
||||
} catch (Throwable $throwable) {
|
||||
$payload = $basePayload + [
|
||||
'success' => false,
|
||||
'duration_ms' => (int) round((microtime(true) - $startedAt) * 1000),
|
||||
'error' => $throwable->getMessage(),
|
||||
];
|
||||
self::writeCronLog($payload);
|
||||
if ((bool) ($options['json'] ?? false)) {
|
||||
self::emitJson($payload);
|
||||
} else {
|
||||
fwrite(STDERR, '[ERROR] ' . $jobName . ' (' . $mode . '): ' . $throwable->getMessage() . PHP_EOL);
|
||||
}
|
||||
return 1;
|
||||
} finally {
|
||||
self::releaseLock($lockHandle);
|
||||
}
|
||||
}
|
||||
|
||||
public static function printUsage(string $usage): void
|
||||
{
|
||||
fwrite(STDOUT, trim($usage) . PHP_EOL);
|
||||
}
|
||||
|
||||
public static function printOptionError(string $message, string $usage): void
|
||||
{
|
||||
fwrite(STDERR, $message . PHP_EOL . PHP_EOL . trim($usage) . PHP_EOL);
|
||||
}
|
||||
|
||||
private static function acquireLock(string $jobName): array
|
||||
{
|
||||
$lockDir = self::logsDir() . '/locks';
|
||||
if (!is_dir($lockDir)) {
|
||||
@mkdir($lockDir, 0775, true);
|
||||
}
|
||||
|
||||
$lockPath = $lockDir . '/task_cert_' . $jobName . '.lock';
|
||||
$handle = @fopen($lockPath, 'c+');
|
||||
if ($handle === false) {
|
||||
throw new RuntimeException('Lock-Datei konnte nicht geöffnet werden: ' . $lockPath);
|
||||
}
|
||||
|
||||
$acquired = flock($handle, LOCK_EX | LOCK_NB);
|
||||
if (!$acquired) {
|
||||
return [
|
||||
'acquired' => false,
|
||||
'handle' => $handle,
|
||||
];
|
||||
}
|
||||
|
||||
$meta = [
|
||||
'pid' => getmypid(),
|
||||
'started_at' => date('c'),
|
||||
];
|
||||
ftruncate($handle, 0);
|
||||
rewind($handle);
|
||||
fwrite($handle, json_encode($meta, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL);
|
||||
fflush($handle);
|
||||
|
||||
return [
|
||||
'acquired' => true,
|
||||
'handle' => $handle,
|
||||
];
|
||||
}
|
||||
|
||||
private static function releaseLock($lockHandle): void
|
||||
{
|
||||
if (!is_resource($lockHandle)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@flock($lockHandle, LOCK_UN);
|
||||
@fclose($lockHandle);
|
||||
}
|
||||
|
||||
private static function writeCronLog(array $payload): void
|
||||
{
|
||||
$logDir = self::logsDir();
|
||||
if (!is_dir($logDir)) {
|
||||
@mkdir($logDir, 0775, true);
|
||||
}
|
||||
|
||||
$file = $logDir . '/task_cert_cron.log';
|
||||
$line = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES);
|
||||
if (!is_string($line) || $line === '') {
|
||||
$line = json_encode([
|
||||
'timestamp' => date('c'),
|
||||
'success' => false,
|
||||
'error' => 'cron_log_encode_failed',
|
||||
]);
|
||||
}
|
||||
|
||||
@file_put_contents($file, $line . PHP_EOL, FILE_APPEND | LOCK_EX);
|
||||
}
|
||||
|
||||
private static function emitJson(array $payload): void
|
||||
{
|
||||
echo json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) . PHP_EOL;
|
||||
}
|
||||
|
||||
private static function logsDir(): string
|
||||
{
|
||||
return dirname(__DIR__) . '/logs';
|
||||
}
|
||||
|
||||
private static function buildRunId(string $jobName): string
|
||||
{
|
||||
try {
|
||||
$entropy = bin2hex(random_bytes(6));
|
||||
} catch (Throwable $throwable) {
|
||||
$entropy = substr(sha1((string) microtime(true)), 0, 12);
|
||||
}
|
||||
|
||||
return $jobName . '-' . date('YmdHis') . '-' . $entropy;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user