Files
kefu/im/server-fix-20260831/deploy_cache.py
T
2026-09-03 08:38:17 +08:00

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)