= 8) break; } } return $failures; } function bratonien_tools_webdav_dispatch_only_connector_deferrals(array $output) { $seen = false; foreach ($output as $line) { $line = trim((string)$line); if ($line === '') continue; if (strpos($line, '[BRAT-WORKER] fatal ') !== false || strpos($line, 'download_failed') !== false || stripos($line, 'RESTORE FEHLGESCHLAGEN') !== false) { return false; } if (strpos($line, ' ok=0 ') !== false) { $seen = true; if (strpos($line, 'message=Connector-Synchronisierung läuft.') === false && strpos($line, 'message=Connector-Mapping hat sich seit dem Batch-Download geändert.') === false && strpos($line, 'message=Shadow-Tree hat sich seit dem Batch-Download geändert.') === false) { return false; } } } return $seen; } function bratonien_tools_webdav_dispatch_connector_service_active() { if (!function_exists('exec') || !is_executable('/usr/bin/systemctl')) return false; $output = array(); $exit = 1; @exec('/usr/bin/systemctl is-active --quiet bratonien-nc-connector.service 2>/dev/null', $output, $exit); return $exit === 0; } function bratonien_tools_webdav_dispatch_reset_orphan_sync_lock($state_dir) { $path = rtrim((string)$state_dir, '/').'/webdav-sync.lock'; if (!is_file($path)) return true; $lock = @fopen($path, 'c'); if (!$lock) return false; if (@flock($lock, LOCK_SH | LOCK_NB)) { @flock($lock, LOCK_UN); fclose($lock); return true; } fclose($lock); // Der Produktions-Connector darf ausschließlich über den systemd-Dienst // laufen. Ist dieser nicht aktiv, ist ein weiterhin exklusiv belegter Lock // kein gültiger Connector-Zustand. Den alten Inode hängen wir aus und legen // eine frische Lockdatei an, damit ein verwaister Altprozess den Cache nicht // dauerhaft blockiert. Ein späterer echter Connector benutzt automatisch die // neue Datei. if (bratonien_tools_webdav_dispatch_connector_service_active()) return false; $orphan = $path.'.orphan-'.date('YmdHis').'-'.getmypid(); if (!@rename($path, $orphan)) return false; $fresh = @fopen($path, 'c'); if (!$fresh) { @rename($orphan, $path); return false; } @chmod($path, 0660); fclose($fresh); @file_put_contents(PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.log', '[BRAT-WORKER] orphan_sync_lock_reset old='.basename($orphan)."\n", FILE_APPEND | LOCK_EX); return true; } function bratonien_tools_webdav_dispatch_connector_busy($state_dir) { $path = rtrim((string)$state_dir, '/').'/webdav-sync.lock'; $lock = @fopen($path, 'c'); if (!$lock) return false; if (@flock($lock, LOCK_SH | LOCK_NB)) { @flock($lock, LOCK_UN); fclose($lock); return false; } fclose($lock); if (bratonien_tools_webdav_dispatch_connector_service_active()) return true; // Genau der hier beobachtete Fehlerfall: Lock belegt, aber kein Connector- // Dienst aktiv. Nicht als "Connector läuft" darstellen, sondern den // verwaisten Lockzustand reparieren und den Cache normal starten lassen. if (bratonien_tools_webdav_dispatch_reset_orphan_sync_lock($state_dir)) return false; // Konnte der ungültige Lockzustand nicht sicher repariert werden, wird nicht // behauptet, dass ein Connector läuft. Der Worker erhält stattdessen einen // echten Fehler und kann den Zustand sichtbar machen. throw new RuntimeException('WebDAV-Sync-Lock ist belegt, obwohl der Connector-Dienst nicht aktiv ist, und konnte nicht sicher zurückgesetzt werden.'); } function bratonien_tools_webdav_dispatch_schedule_after_sync($waiter, $state_dir, $worker_command, $log, &$pid=null) { $pid = 0; $deferred = escapeshellarg('/bin/bash').' '.escapeshellarg($waiter).' '.escapeshellarg($state_dir).' '.$worker_command; $command = 'nohup '.$deferred.' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!'; $output = array(); $exit = 1; @exec($command, $output, $exit); $pid = isset($output[0]) ? (int)$output[0] : 0; return $exit === 0 && $pid > 0; } $mode = 'periodic'; $wait = false; $connection_filter = 0; foreach ($argv as $arg) { if (preg_match('/^--mode=(sync|periodic|manual|rebuild)$/', $arg, $m)) $mode = $m[1]; elseif (preg_match('/^--connection-id=(\d+)$/', $arg, $m)) $connection_filter = (int)$m[1]; elseif ($arg === '--wait') $wait = true; } $settings = bratonien_tools_get_webdav_warmup_settings(); if (!in_array($mode, array('manual','rebuild'), true) && empty($settings['enabled'])) { fwrite(STDOUT, "WebDAV-Worker ist deaktiviert.\n"); exit(0); } if (!function_exists('exec')) { fwrite(STDERR, "PHP exec() ist deaktiviert; Worker kann nicht gestartet werden.\n"); exit(1); } $worker = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/webdav-cache-warmup.php'); $priority_waiter = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/run-webdav-priority-wait.sh'); $after_sync_waiter = realpath(BRATONIEN_TOOLS_PATH.'runtime/lib/run-webdav-after-sync.sh'); if (!$worker || !is_file($worker)) { fwrite(STDERR, "WebDAV-Worker wurde nicht gefunden.\n"); exit(1); } if (!$priority_waiter || !is_file($priority_waiter)) { fwrite(STDERR, "WebDAV-Prioritäts-Warter wurde nicht gefunden.\n"); exit(1); } if (!$after_sync_waiter || !is_file($after_sync_waiter)) { fwrite(STDERR, "WebDAV-Connector-Warter wurde nicht gefunden.\n"); exit(1); } $log = PHPWG_ROOT_PATH.PWG_LOCAL_DIR.'bratonien-webdav-warmup.log'; $result = 0; $started = 0; $matched = 0; $run_inline = $wait || in_array($mode, array('manual','rebuild'), true); foreach (bratonien_tools_nc_connector_connections() as $connection) { if (empty($connection['enabled']) || !bratonien_tools_nc_connector_is_webdav($connection)) continue; $connection_id = (int)$connection['id']; if ($connection_id < 1) continue; if ($connection_filter > 0 && $connection_id !== $connection_filter) continue; $matched++; $config = isset($connection['config']) && is_array($connection['config']) ? $connection['config'] : array(); $state_dir = rtrim((string)($config['state_dir'] ?? ''), '/'); if ($state_dir === '') { fwrite(STDERR, "Worker für Verbindung #{$connection_id} übersprungen: Connector-State-Verzeichnis fehlt.\n"); $result = 1; continue; } $worker_command = escapeshellarg(PHP_BINARY).' '.escapeshellarg($worker) .' --connection-id='.$connection_id .' --mode='.escapeshellarg($mode); try { $connector_busy = $mode !== 'sync' && bratonien_tools_webdav_dispatch_connector_busy($state_dir); } catch (Throwable $e) { if (function_exists('bratonien_tools_webdav_warmup_write_status')) { bratonien_tools_webdav_warmup_write_status($connection_id, 'error', $e->getMessage(), array('mode'=>$mode, 'deferred_by_connector'=>false)); } fwrite(STDERR, $e->getMessage()."\n"); $result = 1; continue; } if ($mode === 'sync') { if (!is_dir($state_dir) && !@mkdir($state_dir, 0750, true) && !is_dir($state_dir)) { fwrite(STDERR, "Worker-Priorität für Verbindung #{$connection_id} konnte nicht signalisiert werden: State-Verzeichnis fehlt.\n"); $result = 1; continue; } $priority_file = $state_dir.'/webdav-cache-warmup-priority-sync'; $priority_payload = json_encode(array( 'connection_id'=>$connection_id, 'requested_at'=>time(), 'reason'=>'connector-sync-complete', ), JSON_UNESCAPED_SLASHES); if (!is_string($priority_payload) || @file_put_contents($priority_file, $priority_payload."\n", LOCK_EX) === false) { fwrite(STDERR, "Worker-Priorität für Verbindung #{$connection_id} konnte nicht geschrieben werden.\n"); $result = 1; continue; } @chmod($priority_file, 0664); } elseif ($connector_busy) { $resume_pid = 0; $scheduled = bratonien_tools_webdav_dispatch_schedule_after_sync($after_sync_waiter, $state_dir, $worker_command, $log, $resume_pid); if (function_exists('bratonien_tools_webdav_warmup_write_status')) { bratonien_tools_webdav_warmup_write_status( $connection_id, $scheduled ? 'waiting' : 'error', $scheduled ? 'Connector-Synchronisierung läuft. Der Cache-Worker ist vorgemerkt und startet automatisch unmittelbar danach; es werden vorher keine Nextcloud-Originale geladen.' : 'Connector-Synchronisierung läuft, aber der automatische Cache-Fortsetzungsauftrag konnte nicht gestartet werden.', array('mode'=>$mode, 'deferred_by_connector'=>true, 'resume_scheduled'=>$scheduled, 'resume_pid'=>$resume_pid) ); } if (!$scheduled) { fwrite(STDERR, "Wartender WebDAV-Worker für Verbindung #{$connection_id} konnte nicht gestartet werden.\n"); $result = 1; continue; } $started++; fwrite(STDOUT, "WebDAV-Worker für Verbindung #{$connection_id} wartet auf den Connector-Sync und startet danach automatisch (PID {$resume_pid}).\n"); continue; } $base = $mode === 'sync' ? escapeshellarg('/bin/bash').' '.escapeshellarg($priority_waiter).' '.escapeshellarg($state_dir).' '.$worker_command : $worker_command; if ($run_inline) { $output = array(); $exit = 1; @exec($base.' 2>&1', $output, $exit); foreach ($output as $line) { fwrite(STDOUT, $line."\n"); @file_put_contents($log, $line."\n", FILE_APPEND | LOCK_EX); } if ($exit !== 0) { $deferred = bratonien_tools_webdav_dispatch_only_connector_deferrals($output); if ($deferred) { $resume_pid = 0; $scheduled = bratonien_tools_webdav_dispatch_schedule_after_sync($after_sync_waiter, $state_dir, $worker_command, $log, $resume_pid); if (function_exists('bratonien_tools_webdav_warmup_write_status')) { $existing = bratonien_tools_webdav_dispatch_existing_status($connection_id); $extra = is_array($existing) ? $existing : array(); unset($extra['state'], $extra['message'], $extra['connection_id'], $extra['updated_at']); $extra['worker_exit_code'] = (int)$exit; $extra['deferred_by_connector'] = true; $extra['resume_scheduled'] = $scheduled; $extra['resume_pid'] = $resume_pid; bratonien_tools_webdav_warmup_write_status( $connection_id, $scheduled ? 'waiting' : 'error', $scheduled ? 'Connector-Synchronisierung hat während des Batches Vorrang erhalten. Der bisherige Indexstand bleibt erhalten; der Cache-Worker ist zur automatischen Fortsetzung vorgemerkt.' : 'Connector-Synchronisierung hat während des Batches Vorrang erhalten, aber die automatische Fortsetzung konnte nicht vorgemerkt werden.', $extra ); } if (!$scheduled) $result = 1; else $started++; continue; } $result = $exit; if (function_exists('bratonien_tools_webdav_warmup_write_status')) { $existing = bratonien_tools_webdav_dispatch_existing_status($connection_id); $failures = bratonien_tools_webdav_dispatch_failure_lines($output); $terminal = is_array($existing) && in_array((string)($existing['state'] ?? ''), array('error','fatal','preempted','cancelled'), true); if ($terminal) { $message = trim((string)($existing['message'] ?? 'WebDAV-Worker wurde mit Fehlern beendet.')); if ($failures) { $message .= ' Fehlerbeispiele: '.implode(' | ', $failures); } $extra = $existing; unset($extra['state'], $extra['message'], $extra['connection_id'], $extra['updated_at']); $extra['worker_exit_code'] = (int)$exit; $extra['dispatcher_preserved_worker_status'] = true; if ($failures) $extra['failure_examples'] = $failures; bratonien_tools_webdav_warmup_write_status( $connection_id, (string)$existing['state'], $message, $extra ); } else { $detail = $failures ? implode(' | ', $failures) : ($output ? implode(' ', array_slice($output, -8)) : 'keine Worker-Ausgabe'); bratonien_tools_webdav_warmup_write_status( $connection_id, 'error', 'WebDAV-Worker Exit '.(int)$exit.'. Ursache: '.$detail, array('mode'=>$mode, 'worker_exit_code'=>(int)$exit, 'failure_examples'=>$failures) ); } } } else { $started++; } } else { $command = 'nohup '.$base.' >> '.escapeshellarg($log).' 2>&1 < /dev/null & echo $!'; $output = array(); $exit = 1; @exec($command, $output, $exit); $pid = isset($output[0]) ? (int)$output[0] : 0; if ($exit !== 0 || $pid <= 0) { fwrite(STDERR, "Worker für Verbindung #{$connection_id} konnte nicht gestartet werden.\n"); $result = 1; continue; } $started++; fwrite(STDOUT, "WebDAV-Worker {$mode} für Verbindung #{$connection_id} gestartet (PID {$pid}).\n"); } } if ($connection_filter > 0 && $matched === 0) { fwrite(STDERR, "WebDAV-Verbindung #{$connection_filter} wurde nicht als aktive Connector-Verbindung gefunden.\n"); exit(1); } if (!$run_inline) fwrite(STDOUT, "Gestartete WebDAV-Worker: {$started}.\n"); exit($result);