/dev/null', $out, $rc); if ($rc === 0) return true; $ps = @shell_exec('ps -eo args= 2>/dev/null'); if (!is_string($ps) || $ps === '') return false; foreach (explode("\n", $ps) as $line) { if (strpos($line, 'mvlog_worker.py') !== false && strpos($line, '--remote-root /var/www/html') !== false) { return true; } } return false; } function mvlog_rm_rf(string $path): void { if (!is_dir($path)) return; $items = scandir($path); if (!is_array($items)) return; foreach ($items as $item) { if ($item === '.' || $item === '..') continue; $child = $path . '/' . $item; if (is_dir($child) && !is_link($child)) mvlog_rm_rf($child); else @unlink($child); } @rmdir($path); } function mvlog_input_dir_state(string $dir): array { $stateFile = $dir . '/.movmaker-state.json'; $state = is_file($stateFile) ? json_decode((string)@file_get_contents($stateFile), true) : []; return is_array($state) ? $state : []; } function mvlog_input_dir_preview(string $dir): bool { if (is_file($dir . '/.movmaker-preview')) return true; $state = mvlog_input_dir_state($dir); return !empty($state['preview']); } function mvlog_input_dir_enabled(string $dir): bool { return is_file($dir . '/.movmaker-enabled'); } function mvlog_input_dir_visible_marker(string $dir): bool { return !is_file($dir . '/.mvlog-hidden'); } function mvlog_set_input_dir_visible(string $dir, bool $visible): void { $path = $dir . '/.mvlog-hidden'; if ($visible) { if (is_file($path)) @unlink($path); } else { @file_put_contents($path, "hidden "); @chmod($path, 0664); } } function mvlog_input_dir_permalink(string $dir): bool { return is_file($dir . '/.mvlog-permalink'); } function mvlog_input_dir_has_full_output(string $dir, array $config): bool { $state = mvlog_input_dir_state($dir); $output = basename((string)($state['output'] ?? '')); return $output !== '' && is_file($config['videos_dir'] . '/' . $output) && !preg_match('/_preview\.(mp4|webm|mov|m4v)$/i', $output); } function mvlog_input_dir_can_show(string $dir, array $config): bool { return mvlog_input_dir_has_full_output($dir, $config) && !mvlog_input_dir_preview($dir); } function mvlog_initialize_input_dir_defaults(string $dir, array $config): void { if (!is_file($dir . '/.mvlog-hidden') && !mvlog_input_dir_permalink($dir) && !mvlog_input_dir_has_full_output($dir, $config)) { mvlog_set_input_dir_visible($dir, false); } } function mvlog_input_dir_switch_payload(string $dir, array $config, string $articleId): array { $canShow = mvlog_input_dir_can_show($dir, $config); $visible = $canShow && mvlog_input_dir_visible_marker($dir); $permalink = mvlog_input_dir_permalink($dir); $running = is_dir($dir . '/.movmaker-lock'); return [ 'dir' => basename($dir), 'id' => $articleId, 'enabled' => mvlog_input_dir_enabled($dir), 'preview' => mvlog_input_dir_preview($dir), 'visible' => $visible, 'permalink' => $permalink, 'can_show' => $canShow, 'can_permalink' => $canShow, 'public_locked' => $visible || $permalink, 'map_hidden' => is_file($dir . '/.mvlog-hide-map'), 'running' => $running, ]; } function mvlog_cleanup_stale_processing(string $dir, string $name, array $state, string $lockDir, bool $locked, string $status, string $articleId): array { $stateFile = $dir . '/.movmaker-state.json'; $started = ''; if (is_file($lockDir . '/started_at')) $started = trim((string)@file_get_contents($lockDir . '/started_at')); if ($started === '') $started = (string)($state['started_at'] ?? $state['updated_at'] ?? ''); $backup = ''; if (is_file($stateFile)) { $backup = $stateFile . '.bak.' . date('YmdHis'); @copy($stateFile, $backup); @chmod($backup, 0664); } $state['status'] = 'error'; $state['updated_at'] = gmdate('c'); if ($articleId !== '') $state['article_id'] = $articleId; $state['message'] = 'Previous render appears abandoned: no mvlog-worker process is running; stale lock/processing state was cleaned by job_status.php. Re-enable Render to retry.'; $json = json_encode($state, JSON_UNESCAPED_UNICODE | JSON_PRETTY_PRINT) . "\n"; @file_put_contents($stateFile, $json, LOCK_EX); @chmod($stateFile, 0664); if ($locked) mvlog_rm_rf($lockDir); mvlog_job_status_log('cleaned stale processing job=' . $name . ' article_id=' . ($articleId ?: '-') . ' started_at=' . ($started ?: '-') . ' status=' . ($status ?: '-') . ' lock=' . ($locked ? 'yes' : 'no') . ' backup=' . ($backup ?: '-')); return $state; } $jobs = []; $switches = []; $cleaned = []; $workerRunning = mvlog_worker_running(); $dirs = glob($config['uploads_dir'] . '/*', GLOB_ONLYDIR) ?: []; foreach ($dirs as $dir) { $name = basename($dir); mvlog_initialize_input_dir_defaults($dir, $config); $stateFile = $dir . '/.movmaker-state.json'; $lockDir = $dir . '/.movmaker-lock'; $idFile = $dir . '/.mvlog-id'; $state = mvlog_input_dir_state($dir); $status = (string)($state['status'] ?? ''); $articleId = strtolower((string)($state['article_id'] ?? '')); if ($articleId === '' && is_file($idFile)) $articleId = strtolower(trim((string)file_get_contents($idFile))); if (!preg_match('/^[0-9]{14}[a-f0-9]{16}$/', $articleId)) $articleId = ''; $locked = is_dir($lockDir); if (!$workerRunning && ($locked || $status === 'processing')) { $state = mvlog_cleanup_stale_processing($dir, $name, $state, $lockDir, $locked, $status, $articleId); $status = (string)($state['status'] ?? ''); $locked = is_dir($lockDir); $cleaned[] = ['dir' => $name, 'id' => $articleId, 'status' => $status]; } $switches[] = mvlog_input_dir_switch_payload($dir, $config, $articleId); if ($locked) { $started = ''; if (is_file($lockDir . '/started_at')) $started = trim((string)file_get_contents($lockDir . '/started_at')); if ($started === '') $started = (string)($state['started_at'] ?? $state['updated_at'] ?? ''); $jobs[] = [ 'name' => $name, 'id' => $articleId, 'status' => $status ?: ($locked ? 'processing' : 'unknown'), 'started_at' => $started, ]; } } echo json_encode(['jobs' => $jobs, 'switches' => $switches, 'cleaned' => $cleaned, 'worker_running' => $workerRunning, 'checked_at' => date('c')], JSON_UNESCAPED_UNICODE);