false, 'message' => lang('sse_method_not_allowed'), ], JSON_UNESCAPED_UNICODE); exit; } // SSE 헤더 설정 header('Content-Type: text/event-stream'); header('Cache-Control: no-cache'); header('Connection: keep-alive'); header('X-Accel-Buffering: no'); // Nginx 버퍼링 비활성화 // 출력 버퍼 비활성화 (즉시 전송) @ini_set('output_buffering', 'off'); @ini_set('zlib.output_compression', false); if (function_exists('apache_setenv')) { @apache_setenv('no-gzip', 1); } while (ob_get_level() > 0) { ob_end_clean(); } // 타임아웃 설정 (10분) set_time_limit(600); // 클라이언트 연결 끊김 시 즉시 워커 종료 (이슈 #319 부수 발견) // 폴링 풀백과 SSE 워커 동시 실행으로 인한 race condition 차단. // PHP_INI default 는 0(false) 이지만 명시적으로 설정하여 환경 차이를 봉합. ignore_user_abort(false); // 출력 즉시 flush — disconnect 감지를 빠르게 트리거 (flush 시 EPIPE 발생). @ob_implicit_flush(true); // 오류를 SSE 이벤트로 전송하기 위해 오류 출력 비활성화 ini_set('display_errors', '0'); error_reporting(E_ALL); // Worker 시작 로그 (디버깅용) addLog('=== Install Worker SSE Started ==='); addLog('Client IP: '.($_SERVER['REMOTE_ADDR'] ?? 'unknown')); // 워커 lock 획득 — 다른 워커가 활동 중이면 진입 거부 (race 차단). // SSE 응답으로 거부 사유 전달 후 종료. $lockResult = acquireWorkerLock(15); if (! $lockResult['acquired']) { addLog('=== SSE Worker rejected — another worker is active ==='); setProgressEmitter(new SseEmitter); sendSSEEvent('aborted', [ 'message' => lang('error_worker_busy'), 'reason' => 'busy', ]); exit; } $workerId = $lockResult['worker_id']; addLog('Worker lock acquired: '.$workerId.' (reason: '.$lockResult['reason'].')'); // 워커 종료 시 lock 자동 해제 register_shutdown_function('releaseWorkerLock', $workerId); // SSE emitter 등록 후 task runner 실행 setProgressEmitter(new SseEmitter($workerId)); runInstallationTasks();