Files
Gnuboard7/public/install/api/install-worker.php
T
HeuJung 01d319e74b fix(core,installer): 프로세스 출력 인코딩으로 인한 JSON 빈 응답 차단
한국어 Windows 에서 whoami 가 CP949 로 출력해 계정명에 한글이 있으면
invalid UTF-8 이 응답 배열에 실리고 json_encode 가 false 를 반환한다.
echo false 는 빈 문자열이라 HTTP 200 + 빈 본문이 나가는데 예외도 로그도
남지 않아, 설치 마법사 2단계가 진행 불가 상태로 멈춘다.

2026-07 수정(7.0.2)은 로그 축 4곳만 막았다. 출처에서 정규화하고(1차),
응답 경계를 단일 헬퍼로 닫고(2차), 실패 시 문제 필드 경로를 로그에
남기도록(3차) 세 층으로 처리해 다른 경로가 열린 채 남지 않게 했다.

정규화는 복원 가능한 코드페이지 출력을 먼저 되살리므로, 종전 mb_scrub
단독 처리와 달리 한글이 U+FFFD 로 훼손되지 않는다.

같은 원인으로 한국어 Windows 에서 관리자 환경설정의 시스템 정보가
500 이 되던 것도 함께 막았다.
2026-08-29 15:13:14 +09:00

119 lines
4.8 KiB
PHP

<?php
/**
* 그누보드7 웹 인스톨러 - SSE 기반 설치 작업 워커 (진입점)
*
* Server-Sent Events(SSE)를 사용하여 실시간으로 설치 진행 상태를 스트리밍합니다.
* 브라우저 연결이 끊어지면 즉시 설치를 중단합니다.
*
* 실제 작업 실행 로직은 includes/task-runner.php의 runInstallationTasks()가 담당하며,
* 이 파일은 SSE 헤더 설정과 SseEmitter 등록만 수행합니다.
*
* ============================================================================
* Apache + mod_fcgid 환경에서 SSE 미동작 트러블슈팅
* ============================================================================
*
* 증상: 응답 헤더는 즉시 도착하나 body 가 응답 종료 시점에 한 번에 송출되어
* 브라우저가 SSE 이벤트를 실시간 수신 못함. 인스톨러가 폴링 모드로 fallback.
*
* 원인: mod_fcgid 의 `FcgidOutputBufferSize` directive default 가 64KB 라서
* PHP-CGI 의 stdout chunk 가 64KB 미만이면 mod_fcgid 가 응답 종료까지
* buffer 에 hold. PHP 측 ob_implicit_flush / ini_set('output_buffering','off')
* 는 mod_fcgid 레이어를 못 뚫지만, 64KB 임계 padding 송출은 실측상 즉시 flush
* 를 트리거함 (curl ttfb 측정으로 확인).
*
* 해결:
* - 권장: Apache 의 fcgid.conf 에 `FcgidOutputBufferSize 0` 추가 후 재시작 — SSE 매
* 이벤트가 즉시 client 에 도달하므로 본질적 해결.
* - 코드 워크어라운드 (폴링 분기 install-process.php 적용): 응답 echo 직후 64KB
* 공백 padding 을 송출하여 mod_fcgid 임계 강제 통과. 폴링은 응답 1회만 송출하므로
* overhead 무시 가능. SSE 는 매 이벤트마다 padding 시 수십 MB 인플레이션이라
* 적용하지 않음 — SSE 호환성 사전 체크(sse-probe.php) 가 buffered 환경을 감지하면
* 클라이언트가 폴링 모드로 fallback.
* ============================================================================
*/
require_once __DIR__.'/../includes/config.php';
require_once __DIR__.'/../includes/functions.php';
require_once __DIR__.'/../includes/installer-state.php';
require_once __DIR__.'/../includes/progress-emitter.php';
require_once __DIR__.'/../includes/task-runner.php';
require_once __DIR__.'/_guard.php';
installer_guard_or_410();
// SSE는 세션을 사용하지 않음 (세션 잠금 방지)
$state = getInstallationState();
$lang = $state['g7_locale'] ?? 'ko';
$translations = loadTranslations($lang);
// GET 요청만 허용 (SSE는 GET 기반)
if ($_SERVER['REQUEST_METHOD'] !== 'GET') {
http_response_code(405);
header('Content-Type: application/json');
echo installer_json_encode([
'success' => 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();