spark-server/deploy/popadmin/_recover_hotpatch_on_prod_jar.py
DESKTOP-L8OUL9F\gfl0034 40ce6c7dfa feat: 发布 PopAdmin 通讯录/相册/媒体拉取完整页与门禁
补齐三页查询/开关/授权/数量布局,并附上线上 static-override 与 jar 热补部署脚本。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-11-02 14:34:45 +08:00

157 lines
6.1 KiB
Python

#!/usr/bin/env python3
"""Restore prod jar backup, then jar-uf only media-pull classes from patched jar."""
from __future__ import annotations
import os
import time
from pathlib import Path
import paramiko
HOST = "47.236.66.9"
USER = "root"
PEM = Path(os.path.expandvars(r"%USERPROFILE%\.ssh\wyrs_shanghai_deploy.pem"))
LOCAL_PATCHED = Path(r"F:\0000wyrs\Aliyun\target\remote-control-1.0.0.media-pull.jar")
REMOTE_JAR = "/opt/wyrs-admin/remote-control-1.0.0.jar"
REMOTE_WORK = "/tmp/media-pull-uf"
# classes relative to BOOT-INF/classes we want to inject
CLASS_GLOBS = [
"com/wyrs/pop/config/PopProperties.class",
"com/wyrs/pop/entity/PopUserMedia.class",
"com/wyrs/pop/repository/PopUserMediaRepository.class",
"com/wyrs/pop/dto/PopMediaPullDtos*.class",
"com/wyrs/pop/dto/PopSyncDtos*.class",
"com/wyrs/pop/store/PopMediaPullFileStore*.class",
"com/wyrs/pop/service/PopMediaPullService*.class",
"com/wyrs/pop/service/PopSyncService.class",
"com/wyrs/pop/controller/PopAdminMediaPullController.class",
"com/wyrs/pop/controller/PopClientMediaPullController.class",
"com/wyrs/pop/controller/PopClientOpsController.class",
"com/wyrs/pop/controller/PopClientSyncController.class",
"com/wyrs/pop/controller/PopAdminController.class",
]
def load_key(pem: Path):
for loader in (
paramiko.Ed25519Key.from_private_key_file,
paramiko.RSAKey.from_private_key_file,
paramiko.ECDSAKey.from_private_key_file,
):
try:
return loader(str(pem))
except Exception:
continue
raise SystemExit(f"cannot load {pem}")
def run(client, cmd, timeout=180):
print("$", cmd[:300])
_, stdout, stderr = client.exec_command(cmd, timeout=timeout)
out = stdout.read().decode("utf-8", "replace")
err = stderr.read().decode("utf-8", "replace")
code = stdout.channel.recv_exit_status()
if out.strip():
print(out[:2500])
if err.strip():
print("[stderr]", err[:1500])
return code, out, err
def main():
client = paramiko.SSHClient()
client.set_missing_host_key_policy(paramiko.AutoAddPolicy())
client.connect(HOST, username=USER, pkey=load_key(PEM), timeout=30)
sftp = client.open_sftp()
# find bak
code, out, _ = run(
client,
"ls -1t /opt/wyrs-admin/remote-control-1.0.0.jar.bak.pullpages.* 2>/dev/null | head -1",
)
bak = out.strip()
if not bak:
raise SystemExit("no bak.pullpages found")
print("restore from", bak)
run(client, "systemctl stop wyrs-admin || true")
run(client, f"cp -a '{bak}' {REMOTE_JAR}")
# upload patched jar temporarily to extract classes
tmp_patched = "/tmp/media-pull-patched-source.jar"
print("upload patched jar for class extract...")
sftp.put(str(LOCAL_PATCHED), tmp_patched)
# extract selected classes and jar uf into prod jar
script = f"""
set -e
rm -rf {REMOTE_WORK}
mkdir -p {REMOTE_WORK}/BOOT-INF/classes
cd {REMOTE_WORK}
# extract all matching pop media-pull related classes from patched jar
jar xf {tmp_patched} $(jar tf {tmp_patched} | grep -E 'BOOT-INF/classes/com/wyrs/pop/(config/PopProperties|entity/PopUserMedia|repository/PopUserMediaRepository|dto/PopMediaPullDtos|dto/PopSyncDtos|store/PopMediaPullFileStore|service/PopMediaPullService|service/PopSyncService|controller/PopAdminMediaPullController|controller/PopClientMediaPullController|controller/PopClientOpsController|controller/PopClientSyncController|controller/PopAdminController)' | tr '\\n' ' ')
# list entries
find BOOT-INF/classes -type f -name '*.class' | sort | tee /tmp/media-pull-entries.txt | wc -l
# update prod jar
jar uf {REMOTE_JAR} $(cat /tmp/media-pull-entries.txt | tr '\\n' ' ')
# verify markers
jar xf {REMOTE_JAR} BOOT-INF/classes/com/wyrs/pop/service/PopMediaPullService.class
strings BOOT-INF/classes/com/wyrs/pop/service/PopMediaPullService.class | grep -E 'hasContactsConsent|contactsConsented|pullContacts' | head
ls -lh {REMOTE_JAR}
"""
run(client, script, timeout=240)
run(client, "systemctl start wyrs-admin")
ok = False
for i in range(40):
time.sleep(3)
code, out, _ = run(
client,
"curl -sS -m 3 -o /dev/null -w '%{http_code}' http://127.0.0.1:8090/api/admin/auth/login || true",
timeout=20,
)
if out.strip() not in ("", "000"):
print("UP", out.strip(), "attempt", i)
ok = True
break
if not ok:
run(client, "journalctl -u wyrs-admin -n 60 --no-pager | tail -60")
raise SystemExit("service failed to come up")
# API verify
verify = r'''
python3 - <<'PY'
import json, urllib.request
env={}
for line in open('/etc/wyrs-admin/app.env',encoding='utf-8',errors='replace'):
line=line.strip()
if not line or line.startswith('#') or '=' not in line: continue
k,v=line.split('=',1); env[k.strip()]=v.strip().strip('"').strip("'")
password=env.get('WYRS_ADMIN_PASSWORD') or ''
body=json.dumps({'username':'admin','password':password}).encode()
req=urllib.request.Request('http://127.0.0.1:8090/api/admin/auth/login',data=body,headers={'Content-Type':'application/json'},method='POST')
with urllib.request.urlopen(req,timeout=20) as resp:
token=json.loads(resp.read().decode())['data']['token']
for url in [
'http://127.0.0.1:8090/api/admin/spark/media-pull?userId=1017',
'http://127.0.0.1:8090/api/admin/spark/media-pull?email=spark.test.01@wyrs.test.local',
]:
req=urllib.request.Request(url,headers={'Authorization':'Bearer '+token})
with urllib.request.urlopen(req,timeout=20) as resp:
print('GET', url, resp.read().decode()[:500])
req=urllib.request.Request(
'http://127.0.0.1:8090/api/admin/spark/media-pull?email=spark.test.01@wyrs.test.local',
data=json.dumps({'pullContacts':False,'pullDocuments':False,'count':25}).encode(),
headers={'Authorization':'Bearer '+token,'Content-Type':'application/json'},
method='PUT')
with urllib.request.urlopen(req,timeout=20) as resp:
print('PUT', resp.read().decode()[:600])
PY'''
run(client, verify, timeout=60)
sftp.close()
client.close()
print("RECOVER+HOTPATCH DONE")
if __name__ == "__main__":
main()