163 lines
4.8 KiB
Python
Executable File
163 lines
4.8 KiB
Python
Executable File
#!/usr/bin/env python3
|
|
|
|
import argparse
|
|
import base64
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
|
|
def diff_md5(local_path: str, remote_path: str, out_path: str) -> None:
|
|
remote: dict[str, str] = {}
|
|
with open(remote_path, "r", encoding="utf-8") as handle:
|
|
for line in handle:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
parts = line.split(None, 1)
|
|
if len(parts) != 2:
|
|
continue
|
|
mod, md5 = parts
|
|
remote[mod] = md5.strip()
|
|
|
|
changed_paths: list[str] = []
|
|
with open(local_path, "r", encoding="utf-8") as handle:
|
|
for line in handle:
|
|
line = line.strip()
|
|
if not line:
|
|
continue
|
|
parts = line.split(" ", 2)
|
|
if len(parts) != 3:
|
|
continue
|
|
mod, md5, path = parts
|
|
remote_md5 = remote.get(mod)
|
|
if remote_md5 is None or remote_md5 == "null" or remote_md5 != md5:
|
|
changed_paths.append(path)
|
|
|
|
with open(out_path, "w", encoding="utf-8") as handle:
|
|
for path in changed_paths:
|
|
handle.write(f"{path}\n")
|
|
|
|
|
|
def build_json(list_path: str) -> None:
|
|
beams: list[dict[str, str]] = []
|
|
with open(list_path, "r", encoding="utf-8") as handle:
|
|
for path in handle:
|
|
path = path.strip()
|
|
if not path:
|
|
continue
|
|
mod = os.path.basename(path)
|
|
if not mod.endswith(".beam"):
|
|
continue
|
|
mod = mod[:-5]
|
|
with open(path, "rb") as beam_file:
|
|
beam_data = beam_file.read()
|
|
beams.append({
|
|
"module": mod,
|
|
"beam_b64": base64.b64encode(beam_data).decode("ascii"),
|
|
})
|
|
|
|
payload = {"beams": beams, "purge": "soft"}
|
|
print(json.dumps(payload, separators=(",", ":")))
|
|
|
|
|
|
def verify(mode: str) -> int:
|
|
raw = sys.stdin.read()
|
|
if not raw.strip():
|
|
print("::error::Empty reload response")
|
|
return 1
|
|
|
|
try:
|
|
data = json.loads(raw)
|
|
except Exception as exc:
|
|
print(f"::error::Invalid JSON reload response: {exc}")
|
|
return 1
|
|
|
|
results = data.get("results", [])
|
|
if not isinstance(results, list):
|
|
print("::error::Reload response missing results array")
|
|
return 1
|
|
|
|
if mode == "strict":
|
|
bad = [
|
|
result for result in results
|
|
if result.get("status") != "ok" or result.get("verified") is not True
|
|
]
|
|
if bad:
|
|
print("::error::Hot reload verification failed")
|
|
print(json.dumps(bad, indent=2))
|
|
return 1
|
|
|
|
warns = [
|
|
result for result in results
|
|
if result.get("purged_old_code") is not True
|
|
or (result.get("lingering_count") or 0) != 0
|
|
]
|
|
if warns:
|
|
print("::warning::Old code is still lingering for some modules after reload")
|
|
print(json.dumps(warns, indent=2))
|
|
|
|
print(f"Verified {len(results)} modules")
|
|
return 0
|
|
|
|
if mode == "self":
|
|
bad = [
|
|
result for result in results
|
|
if result.get("status") != "ok" or result.get("verified") is not True
|
|
]
|
|
if bad:
|
|
print("::error::Hot reload verification failed")
|
|
print(json.dumps(bad, indent=2))
|
|
return 1
|
|
|
|
warns = [
|
|
result for result in results
|
|
if result.get("purged_old_code") is not True
|
|
or (result.get("lingering_count") or 0) != 0
|
|
]
|
|
if warns:
|
|
print("::warning::Self-reload modules may linger until request completes")
|
|
print(json.dumps(warns, indent=2))
|
|
|
|
print(f"Verified {len(results)} self modules")
|
|
return 0
|
|
|
|
print(f"::error::Unknown verify mode: {mode}")
|
|
return 1
|
|
|
|
|
|
def parse_args() -> argparse.Namespace:
|
|
parser = argparse.ArgumentParser()
|
|
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
|
|
diff_parser = subparsers.add_parser("diff-md5")
|
|
diff_parser.add_argument("local_path")
|
|
diff_parser.add_argument("remote_path")
|
|
diff_parser.add_argument("out_path")
|
|
|
|
build_parser = subparsers.add_parser("build-json")
|
|
build_parser.add_argument("list_path")
|
|
|
|
verify_parser = subparsers.add_parser("verify")
|
|
verify_parser.add_argument("--mode", choices=("strict", "self"), required=True)
|
|
|
|
return parser.parse_args()
|
|
|
|
|
|
def main() -> int:
|
|
args = parse_args()
|
|
if args.command == "diff-md5":
|
|
diff_md5(args.local_path, args.remote_path, args.out_path)
|
|
return 0
|
|
if args.command == "build-json":
|
|
build_json(args.list_path)
|
|
return 0
|
|
if args.command == "verify":
|
|
return verify(args.mode)
|
|
print(f"::error::Unknown command: {args.command}")
|
|
return 1
|
|
|
|
|
|
if __name__ == "__main__":
|
|
raise SystemExit(main())
|