7.0.11 인스톨러·코어 업데이트 변경(e60a82d73)에 대해 과거 회귀 22건을 부류별로 대조한
결과, 신규 노출면 1건과 테스트 위생 1건이 나와 인터뷰 결정대로 조치했다.
1. argv 채널의 SAPI 게이트 — CGI/FPM 은 register_argc_argv=On 이면 $_SERVER['argv'] 를
쿼리스트링을 '+' 로 쪼개 채우므로(`GET /?x+core:update` → argv[1]==='core:update', php-cgi
실측) 비인증 웹 요청이 업데이트 트리로 판정되어 bootstrap/app.php 자가 치유가 요청마다
패키지 매니페스트를 지우고 다시 만들었다. CoreUpdateContext 와 bootstrap/app.php 복제본
모두 argv 를 cli·phpdbg 에서만 읽는다. env 플래그 채널은 웹에서 주입할 수 없으므로 그대로
두어 웹 요청 안에서 시작하는 업데이트 흐름(7.1.0)에 영향이 없다. 동형성 테스트에 SAPI 축을
더했다.
2. 매니페스트 삭제 실패 기록 — PackageManifestCacheHelper::clear 가 지우지 못한 파일의
경로를 돌려주고, spawn 직전 호출부가 업그레이드 로그·콘솔에 경고로 남긴다. 권한·소유권
불일치면 자식의 자가 치유도 같은 이유로 실패해 증상은 제보와 같은 「Class not found」 인데,
이 경고가 원인이 권한이라는 유일한 흔적이다.
3. 테스트 격리 — tests/bootstrap.php 가 APP_PACKAGES_CACHE/APP_SERVICES_CACHE 를 테스트
전용 경로로 돌린다. proc_open 으로 자식을 띄우는 기존 테스트 2종의 자식이 개발 클론의
실제 bootstrap/cache 매니페스트를 지우고 다시 쓰던 것(stat 실측)을 부모·자식 함께 막는다.
관리자 [시스템 최적화] 경로(withPreservedContainer 파사드 복원의 미실측 형제 호출처)는
임시 설치본에서 API 로 실측했다 — 200, 설정·라우트 캐시 재생성, 후속 요청 200, 로그 오류 0.
4. 트러블슈팅 사례 ↔ 회귀 테스트 앵커 계약 — 신규 사례는 헤딩에 <!-- case:{영역}-{번호} -->
앵커를 달고 같은 문자열을 그 사례를 잠그는 회귀 테스트에도 남겨야 한다. 사례 번호는
문서마다 1부터 재시작하고 병합으로 중복되므로(이번 리베이스에서도 우리 사례가 develop 과
같은 29 였다가 31 로 밀렸다), 개수만 대조하면 다른 사례를 덮는 테스트도 초록이 된다.
그런데 판정기 check-troubleshooting-test-coverage.cjs 를 부르는 지점이 저장소에 하나도
없었다 — 스크립트 자체 주석에만 실행법이 적혀 있어 아무도 부르지 않으면 영원히 돌지
않았고, 그 사이 위반이 9건 쌓였다(backend 26~31, cache 17~19). 돌지 않는 대조는 아무것도
잠그지 못하므로 위반 해소와 실행 지점 부여를 함께 한다.
9건 전부에 앵커를 부착하고(각 사례가 선언한 회귀 테스트 중 가장 구체적인 파일에 배치,
한 파일이 두 사례에 선언된 경우는 갈라 배치), stop-guard 7.2 에 앵커 계약 + 미커버
baseline ratchet 두 축으로 등록했다. 트러블슈팅 사례 추가 프로토콜에 6단계를
더하고 coverage 에 troubleshooting-case-anchor-contract(manual-only, 전용 판정기 위임)를
등재했다. 판정기 종료코드 1 → 0, 미커버 건수는 전 문서 baseline 그대로다.
74 lines
3.2 KiB
PHP
74 lines
3.2 KiB
PHP
<?php
|
|
|
|
namespace App\Support;
|
|
|
|
use Illuminate\Support\Facades\Artisan;
|
|
|
|
/**
|
|
* 패키지 매니페스트 캐시(bootstrap/cache/packages.php · services.php) 정리·재생성 헬퍼.
|
|
*
|
|
* Laravel 의 `PackageManifest::getManifest()` 는 `packages.php` 가 존재하면 stale 여부를
|
|
* 검사하지 않고 그대로 읽고, `ProviderRepository::load()` 가 거기 등재된 eager provider 를
|
|
* `new` 한다. 그래서 vendor 를 교체한 뒤 그 파일을 남겨 두면 다음에 부팅하는 프로세스가
|
|
* 새 vendor 에 없는 클래스를 찾다 **부팅 단계에서** 죽는다 — 앱 로그가 아직 열리기 전이라
|
|
* 예외도 남지 않고, 부모 프로세스에는 자식의 비정상 종료로만 보인다.
|
|
*
|
|
* `ConfigCacheHelper`/`RouteCacheHelper` 와 같은 자리의 헬퍼로, vendor 를 바꾼 뒤 새 PHP
|
|
* 프로세스를 띄우는 지점이 이 헬퍼를 경유한다. 삭제 로직을 호출부마다 `@unlink` 로 복제하면
|
|
* 한 곳만 빠져도 그 경로가 조용히 옛 매니페스트로 부팅한다.
|
|
*/
|
|
class PackageManifestCacheHelper
|
|
{
|
|
/**
|
|
* 패키지 매니페스트 캐시 두 파일을 삭제합니다 (재생성 없음).
|
|
*
|
|
* 경로는 `Application` 의 게터로 읽어 `APP_PACKAGES_CACHE`/`APP_SERVICES_CACHE` 환경변수
|
|
* 재지정을 존중한다(테스트 격리가 이 경로 재지정에 의존한다).
|
|
*
|
|
* 삭제 실패(권한 · 소유권 불일치 · Windows 파일 핸들 점유)는 예외로 올리지 않는다 — 부팅 직전에
|
|
* 불리므로 실패가 흐름을 막으면 안 되고, 코어 업데이트의 마지막 단계(`clearAllCaches()`)가 다시
|
|
* 시도한다. 대신 지우지 못한 파일의 경로를 돌려준다. spawn 직전 호출부는 그 목록을 업그레이드
|
|
* 로그에 남긴다 — 그 상태면 자식의 자가 치유도 같은 권한으로 같은 이유로 실패해 증상은 이전
|
|
* 설치본 provider 의 「Class not found」 그대로인데, 이 기록이 권한이 원인이라는 유일한 흔적이다.
|
|
*
|
|
* @return array<int, string> 삭제하지 못하고 남은 파일의 절대 경로. 전부 지웠거나 원래 없었으면 빈 배열
|
|
*/
|
|
public static function clear(): array
|
|
{
|
|
$app = app();
|
|
$remaining = [];
|
|
|
|
foreach ([$app->getCachedServicesPath(), $app->getCachedPackagesPath()] as $path) {
|
|
if (! is_file($path)) {
|
|
continue;
|
|
}
|
|
|
|
if (! @unlink($path)) {
|
|
clearstatcache(true, $path);
|
|
if (is_file($path)) {
|
|
$remaining[] = $path;
|
|
}
|
|
}
|
|
}
|
|
|
|
clearstatcache();
|
|
|
|
return $remaining;
|
|
}
|
|
|
|
/**
|
|
* 패키지 매니페스트 캐시를 비우고 현재 vendor 기준으로 다시 만듭니다.
|
|
*
|
|
* `package:discover` 는 새 Application 을 부팅하지 않으므로(현재 앱의 `PackageManifest` 를
|
|
* 그대로 쓴다) `ConfigCacheHelper::withPreservedContainer()` 로 감쌀 필요가 없다.
|
|
*
|
|
* @return void
|
|
*/
|
|
public static function rebuild(): void
|
|
{
|
|
self::clear();
|
|
|
|
Artisan::call('package:discover');
|
|
}
|
|
}
|