116 lines
5.1 KiB
Python
116 lines
5.1 KiB
Python
import hashlib
|
|
import http.client
|
|
import json
|
|
import os
|
|
import shutil
|
|
import stat
|
|
import subprocess
|
|
import tempfile
|
|
import time
|
|
|
|
BASE = '/www/backup/txiaw-mysql-fix-20260831'
|
|
STAGE = BASE + '/stage'
|
|
WEB = '/www/wwwroot/www.txiaw.com'
|
|
PHP = '/www/server/php/56/bin/php'
|
|
TEMPLATE = WEB + '/Tpl/icp/gxl_index.html'
|
|
HELPER = WEB + '/Lib/HomeNewsCache.php'
|
|
RUNTIME = WEB + '/Runtime/TxiawHomeNews'
|
|
QUERY = b"gxl_mysql_news('field:news_id,news_cid,news_name;limit:100;order:news_addtime desc')"
|
|
REPLACEMENT = b"txiaw_home_news_cached()"
|
|
|
|
if not os.path.exists(BASE + '/index-result.json'):
|
|
raise RuntimeError('Verify the new database index before enabling the cache')
|
|
manifest = json.load(open(STAGE + '/baseline.json'))
|
|
baseline = next(item for item in manifest if item['remote'] == TEMPLATE)
|
|
original = open(TEMPLATE, 'rb').read()
|
|
if hashlib.sha256(original).hexdigest() != baseline['sha256']:
|
|
raise RuntimeError('Homepage template changed since inspection; refusing overwrite')
|
|
if os.path.exists(HELPER):
|
|
raise RuntimeError('Unexpected cache helper already exists')
|
|
assert original.count(QUERY) == 1
|
|
modified = original.replace(b'<php>$jishu =' + QUERY, b"<php>require_once APP_PATH . 'HomeNewsCache.php'; $jishu =" + REPLACEMENT, 1)
|
|
assert modified != original and QUERY not in modified
|
|
|
|
for name in ['HomeNewsCache.php', 'test_home_news_cache.php']:
|
|
subprocess.run([PHP, '-n', '-l', STAGE + '/' + name], check=True, timeout=10)
|
|
unit_dir = tempfile.mkdtemp(prefix='cache-unit-', dir=BASE)
|
|
subprocess.run([PHP, '-n', STAGE + '/test_home_news_cache.php', unit_dir], check=True, timeout=10)
|
|
subprocess.run(['python3', STAGE + '/test_cache_concurrency.py'], check=True, timeout=15)
|
|
|
|
def atomic_write(data, target, metadata):
|
|
fd, tmp = tempfile.mkstemp(prefix='.txiaw-repair-', suffix='.tmp', dir=os.path.dirname(target))
|
|
try:
|
|
with os.fdopen(fd, 'wb') as f:
|
|
f.write(data)
|
|
f.flush()
|
|
os.fsync(f.fileno())
|
|
os.chmod(tmp, stat.S_IMODE(metadata.st_mode))
|
|
os.chown(tmp, metadata.st_uid, metadata.st_gid)
|
|
os.replace(tmp, target)
|
|
finally:
|
|
if os.path.exists(tmp):
|
|
os.unlink(tmp)
|
|
|
|
template_info = os.stat(TEMPLATE)
|
|
runtime_info = os.stat(WEB + '/Runtime')
|
|
if not os.path.isdir(RUNTIME):
|
|
os.mkdir(RUNTIME, 0o750)
|
|
os.chown(RUNTIME, runtime_info.st_uid, runtime_info.st_gid)
|
|
|
|
compiled = []
|
|
cache_root = os.path.realpath(WEB + '/Runtime/Cache')
|
|
compiled_backup = BASE + '/compiled-home-before'
|
|
os.makedirs(compiled_backup, mode=0o700, exist_ok=True)
|
|
for root, dirs, files in os.walk(cache_root):
|
|
dirs[:] = [d for d in dirs if not os.path.islink(os.path.join(root, d))]
|
|
for name in files:
|
|
path = os.path.join(root, name)
|
|
if not name.endswith('.php') or os.path.islink(path) or os.path.getsize(path) > 2 * 1024 * 1024:
|
|
continue
|
|
if os.path.commonpath([cache_root, os.path.realpath(path)]) != cache_root:
|
|
raise RuntimeError('Compiled-cache path escaped site cache root')
|
|
data = open(path, 'rb').read()
|
|
if QUERY in data:
|
|
backup = compiled_backup + '/' + hashlib.sha256(path.encode()).hexdigest() + '.php'
|
|
shutil.copy2(path, backup)
|
|
info = os.stat(path)
|
|
os.chmod(backup, 0o600)
|
|
compiled.append((path, backup, info))
|
|
|
|
def request(path):
|
|
c = http.client.HTTPConnection('127.0.0.1', 80, timeout=10)
|
|
started = time.monotonic()
|
|
c.request('GET', path, headers={'Host': 'www.txiaw.com', 'User-Agent': 'Codex-Repair-Check/1.0'})
|
|
r = c.getresponse()
|
|
data = r.read()
|
|
result = {'path': path, 'status': r.status, 'bytes': len(data), 'seconds': round(time.monotonic() - started, 4)}
|
|
c.close()
|
|
if r.status != 200 or len(data) < 1000:
|
|
raise RuntimeError('Homepage check failed: ' + str(result))
|
|
return result
|
|
|
|
try:
|
|
atomic_write(open(STAGE + '/HomeNewsCache.php', 'rb').read(), HELPER, os.stat(WEB + '/Lib/HomeRequestProtection.php'))
|
|
atomic_write(modified, TEMPLATE, template_info)
|
|
for path, backup, info in compiled:
|
|
os.unlink(path)
|
|
probes = [request('/')]
|
|
cache_path = RUNTIME + '/news-list-v1.json'
|
|
entry = json.load(open(cache_path))
|
|
if len(entry['items']) != 100:
|
|
raise RuntimeError('Cache did not preserve the expected 100 list items')
|
|
digest = hashlib.sha256(open(cache_path, 'rb').read()).hexdigest()
|
|
probes.append(request('/?r=codex-cache-key-check'))
|
|
if digest != hashlib.sha256(open(cache_path, 'rb').read()).hexdigest():
|
|
raise RuntimeError('Query parameter unexpectedly changed the fresh list cache')
|
|
except Exception:
|
|
atomic_write(original, TEMPLATE, template_info)
|
|
# Keep the harmless helper available to any in-flight newly compiled template.
|
|
for path, backup, info in compiled:
|
|
atomic_write(open(backup, 'rb').read(), path, info)
|
|
raise
|
|
|
|
result = {'status': 'installed', 'ttl_seconds': 60, 'stale_seconds': 300, 'items': len(entry['items']), 'invalidated_compiled_files': [p for p, _, _ in compiled], 'http_probes': probes, 'query_argument_reuses_cache': True}
|
|
json.dump(result, open(BASE + '/cache-result.json', 'w'), indent=2)
|
|
print('CACHE_DEPLOYED', json.dumps(result), flush=True)
|