2026-07-24 10:39:10 +08:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
|
"""Verify and start the isolated JD HoloLake Code Channel candidate."""
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
import hashlib
|
|
|
|
|
import http.server
|
|
|
|
|
import json
|
|
|
|
|
import os
|
|
|
|
|
import pathlib
|
|
|
|
|
import re
|
|
|
|
|
import shutil
|
|
|
|
|
import sqlite3
|
|
|
|
|
import subprocess
|
|
|
|
|
import tempfile
|
|
|
|
|
import threading
|
|
|
|
|
import time
|
|
|
|
|
import urllib.error
|
|
|
|
|
import urllib.parse
|
|
|
|
|
import urllib.request
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
VERSION = "16.0.1"
|
|
|
|
|
UPSTREAM_FINGERPRINT = "EB114F5E6C0DC2BCDD183550A4B61A2DC5923710"
|
|
|
|
|
STATE_ROOT = pathlib.Path("/var/lib/guanghu/personas/guanghu/hlcc-v16.0.1")
|
|
|
|
|
LEGACY_DB = STATE_ROOT / "data" / "owner-identity-source.db"
|
|
|
|
|
OWNER_NAME = "bingshuo"
|
2026-07-24 11:25:41 +08:00
|
|
|
CHANNEL_REPOSITORY = "guanghu-ice-heart"
|
2026-07-24 10:39:10 +08:00
|
|
|
LEGACY_REPOSITORY_URL = "https://guanghulab.com/fifth-domain/bingshuo/fifth-domain.git"
|
|
|
|
|
SEED_COMMIT_NUMBER = "HLCC-ICE-000001"
|
|
|
|
|
SEED_CONTRIBUTION_NUMBER = "ZY-CONTRIB-20260723-001"
|
|
|
|
|
RELAY_ROOT = "https://guanghubingshuo.com/hlcc-offline/16.0.1"
|
|
|
|
|
BINARY_NAME = f"forgejo-{VERSION}-linux-amd64"
|
|
|
|
|
EXPECTED = {
|
|
|
|
|
BINARY_NAME: "7a4c568136650c10498a9d3d62c7fd630a0cf09c166293ebd78708248f6398fc",
|
|
|
|
|
f"{BINARY_NAME}.asc": "1c0ca36df3adb0a7692b6bdc84d7886001ca0c6d0408e67c9d232d2f33cecc71",
|
|
|
|
|
"forgejo-release-key.asc": "6fae8894c671ce2397cb35fe40c324f73deade6b4cb3cd6cedd1d2b248e0e3ea",
|
|
|
|
|
"forgejo-upstream-all.bundle": "c33bd074d9b2896259e86ebe03ad31ccdd8ff71897beed4320081fa03b15381f",
|
|
|
|
|
"guanghu-code-channel.bundle": "fc53740259d108128e69f5a809cec438ecf3158175617574ba55b8612c5eaa6c",
|
|
|
|
|
"MANIFEST.sha256": "d564c3b600d4b7a199d8a04ce505ceabf81993ca74fa440805601d55e550f185",
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
STATUS = {
|
|
|
|
|
"ok": True,
|
|
|
|
|
"mode": "bootstrap",
|
|
|
|
|
"version": VERSION,
|
|
|
|
|
"code": "HLCC-JD-CANDIDATE-01",
|
|
|
|
|
"ready": False,
|
|
|
|
|
"stage": "starting",
|
|
|
|
|
"package_profile": "full-offline-v16.0.1",
|
|
|
|
|
}
|
|
|
|
|
STATUS_LOCK = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def set_status(**values: object) -> None:
|
|
|
|
|
with STATUS_LOCK:
|
|
|
|
|
STATUS.update(values)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
class HealthHandler(http.server.BaseHTTPRequestHandler):
|
|
|
|
|
def do_GET(self) -> None: # noqa: N802 - BaseHTTPRequestHandler API
|
|
|
|
|
if self.path != "/health":
|
|
|
|
|
self.send_error(404)
|
|
|
|
|
return
|
|
|
|
|
with STATUS_LOCK:
|
|
|
|
|
payload = json.dumps(STATUS, ensure_ascii=False).encode("utf-8")
|
|
|
|
|
self.send_response(200)
|
|
|
|
|
self.send_header("Content-Type", "application/json; charset=utf-8")
|
|
|
|
|
self.send_header("Cache-Control", "no-store")
|
|
|
|
|
self.send_header("Content-Length", str(len(payload)))
|
|
|
|
|
self.end_headers()
|
|
|
|
|
self.wfile.write(payload)
|
|
|
|
|
|
|
|
|
|
def log_message(self, _format: str, *_args: object) -> None:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def sha256(path: pathlib.Path) -> str:
|
|
|
|
|
digest = hashlib.sha256()
|
|
|
|
|
with path.open("rb") as handle:
|
|
|
|
|
for chunk in iter(lambda: handle.read(1024 * 1024), b""):
|
|
|
|
|
digest.update(chunk)
|
|
|
|
|
return digest.hexdigest()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def download_verified(name: str) -> pathlib.Path:
|
|
|
|
|
destination = STATE_ROOT / "release" / name
|
|
|
|
|
if destination.is_file() and sha256(destination) == EXPECTED[name]:
|
|
|
|
|
return destination
|
|
|
|
|
destination.parent.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
temporary = destination.with_suffix(destination.suffix + ".partial")
|
|
|
|
|
temporary.unlink(missing_ok=True)
|
|
|
|
|
request = urllib.request.Request(
|
|
|
|
|
f"{RELAY_ROOT}/{name}",
|
|
|
|
|
headers={"User-Agent": "HoloLake-Code-Channel/16.0.1"},
|
|
|
|
|
)
|
|
|
|
|
with urllib.request.urlopen(request, timeout=45) as response, temporary.open("wb") as output:
|
|
|
|
|
shutil.copyfileobj(response, output, length=1024 * 1024)
|
|
|
|
|
if sha256(temporary) != EXPECTED[name]:
|
|
|
|
|
temporary.unlink(missing_ok=True)
|
|
|
|
|
raise RuntimeError(f"sha256 mismatch: {name}")
|
|
|
|
|
temporary.replace(destination)
|
|
|
|
|
return destination
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def verify_release(files: dict[str, pathlib.Path]) -> None:
|
|
|
|
|
manifest = files["MANIFEST.sha256"].read_text(encoding="utf-8")
|
|
|
|
|
for name in (
|
|
|
|
|
BINARY_NAME,
|
|
|
|
|
f"{BINARY_NAME}.asc",
|
|
|
|
|
"forgejo-release-key.asc",
|
|
|
|
|
"forgejo-upstream-all.bundle",
|
|
|
|
|
"guanghu-code-channel.bundle",
|
|
|
|
|
):
|
|
|
|
|
expected_line = f"{EXPECTED[name]} {name}"
|
|
|
|
|
if expected_line not in manifest.splitlines():
|
|
|
|
|
raise RuntimeError(f"manifest entry mismatch: {name}")
|
|
|
|
|
|
|
|
|
|
gpg_home = STATE_ROOT / "gpg"
|
|
|
|
|
gpg_home.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
gpg_home.chmod(0o700)
|
|
|
|
|
environment = {**os.environ, "GNUPGHOME": str(gpg_home)}
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["gpg", "--batch", "--import", str(files["forgejo-release-key.asc"])],
|
|
|
|
|
check=True,
|
|
|
|
|
env=environment,
|
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
)
|
|
|
|
|
fingerprint = subprocess.run(
|
|
|
|
|
["gpg", "--batch", "--with-colons", "--fingerprint", UPSTREAM_FINGERPRINT],
|
|
|
|
|
check=True,
|
|
|
|
|
env=environment,
|
|
|
|
|
text=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
).stdout
|
|
|
|
|
if f"fpr:::::::::{UPSTREAM_FINGERPRINT}:" not in fingerprint:
|
|
|
|
|
raise RuntimeError("release key fingerprint mismatch")
|
|
|
|
|
subprocess.run(
|
|
|
|
|
[
|
|
|
|
|
"gpg",
|
|
|
|
|
"--batch",
|
|
|
|
|
"--verify",
|
|
|
|
|
str(files[f"{BINARY_NAME}.asc"]),
|
|
|
|
|
str(files[BINARY_NAME]),
|
|
|
|
|
],
|
|
|
|
|
check=True,
|
|
|
|
|
env=environment,
|
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def wait_for_candidate(process: subprocess.Popen[bytes]) -> None:
|
|
|
|
|
url = "http://127.0.0.1:3340/api/healthz"
|
|
|
|
|
for _attempt in range(90):
|
|
|
|
|
if process.poll() is not None:
|
|
|
|
|
raise RuntimeError(f"candidate exited with code {process.returncode}")
|
|
|
|
|
try:
|
|
|
|
|
with urllib.request.urlopen(url, timeout=2) as response:
|
|
|
|
|
payload = json.load(response)
|
|
|
|
|
if payload.get("status") == "pass":
|
|
|
|
|
return
|
|
|
|
|
except Exception: # Candidate is still starting.
|
|
|
|
|
pass
|
|
|
|
|
time.sleep(1)
|
|
|
|
|
raise RuntimeError("candidate readiness timeout")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def immutable_source_commit(script_path: pathlib.Path | None = None) -> str:
|
|
|
|
|
source = (script_path or pathlib.Path(__file__)).resolve()
|
|
|
|
|
matches = [part for part in source.parts if re.fullmatch(r"[0-9a-f]{40}", part)]
|
|
|
|
|
if len(matches) != 1:
|
|
|
|
|
raise RuntimeError("immutable release commit unavailable")
|
|
|
|
|
return matches[0]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def owner_identity_columns(connection: sqlite3.Connection) -> list[str]:
|
|
|
|
|
return [row[1] for row in connection.execute("pragma table_info(user)")]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def migrate_owner_identity(
|
|
|
|
|
legacy_db: pathlib.Path = LEGACY_DB,
|
|
|
|
|
channel_db: pathlib.Path | None = None,
|
|
|
|
|
receipt_path: pathlib.Path | None = None,
|
|
|
|
|
) -> str:
|
|
|
|
|
target = channel_db or STATE_ROOT / "data" / "hlcc.db"
|
|
|
|
|
receipt = receipt_path or STATE_ROOT / "data" / "owner-migration-receipt.json"
|
|
|
|
|
if not legacy_db.is_file() or not target.is_file():
|
|
|
|
|
raise RuntimeError("owner migration database unavailable")
|
|
|
|
|
|
|
|
|
|
legacy = sqlite3.connect(f"file:{legacy_db}?mode=ro", uri=True, timeout=15)
|
|
|
|
|
channel = sqlite3.connect(target, timeout=15)
|
|
|
|
|
try:
|
|
|
|
|
existing = channel.execute(
|
|
|
|
|
"select is_active, is_admin from user where lower_name = ?",
|
|
|
|
|
(OWNER_NAME,),
|
|
|
|
|
).fetchall()
|
|
|
|
|
if existing:
|
|
|
|
|
if len(existing) != 1 or existing[0] != (1, 1):
|
|
|
|
|
raise RuntimeError("channel owner identity mismatch")
|
|
|
|
|
return "already-present"
|
|
|
|
|
|
|
|
|
|
source_rows = legacy.execute(
|
|
|
|
|
"select * from user where lower_name = ?",
|
|
|
|
|
(OWNER_NAME,),
|
|
|
|
|
).fetchall()
|
|
|
|
|
if len(source_rows) != 1:
|
|
|
|
|
raise RuntimeError("legacy owner identity mismatch")
|
|
|
|
|
|
|
|
|
|
old_columns = owner_identity_columns(legacy)
|
|
|
|
|
new_info = list(channel.execute("pragma table_info(user)"))
|
|
|
|
|
new_columns = {row[1] for row in new_info}
|
|
|
|
|
missing = [
|
|
|
|
|
row[1]
|
|
|
|
|
for row in new_info
|
|
|
|
|
if row[3] and row[4] is None and row[1] != "id" and row[1] not in old_columns
|
|
|
|
|
]
|
|
|
|
|
if missing:
|
|
|
|
|
raise RuntimeError("channel owner schema has unsupported required columns")
|
|
|
|
|
|
|
|
|
|
copied_columns = [name for name in old_columns if name in new_columns and name != "id"]
|
|
|
|
|
values = dict(zip(old_columns, source_rows[0]))
|
|
|
|
|
for counter in ("num_repos", "num_stars", "num_followers", "num_following"):
|
|
|
|
|
if counter in values:
|
|
|
|
|
values[counter] = 0
|
|
|
|
|
if "use_custom_avatar" in values:
|
|
|
|
|
values["use_custom_avatar"] = 0
|
|
|
|
|
if "prohibit_login" in values:
|
|
|
|
|
values["prohibit_login"] = 0
|
|
|
|
|
|
|
|
|
|
backup_dir = target.parent / "backups"
|
|
|
|
|
backup_dir.mkdir(parents=True, exist_ok=True)
|
|
|
|
|
backup_path = backup_dir / "hlcc-before-owner-migration.db"
|
|
|
|
|
backup = sqlite3.connect(backup_path)
|
|
|
|
|
try:
|
|
|
|
|
channel.backup(backup)
|
|
|
|
|
finally:
|
|
|
|
|
backup.close()
|
|
|
|
|
backup_path.chmod(0o600)
|
|
|
|
|
|
|
|
|
|
placeholders = ",".join("?" for _name in copied_columns)
|
|
|
|
|
column_sql = ",".join(f'"{name}"' for name in copied_columns)
|
|
|
|
|
with channel:
|
|
|
|
|
channel.execute(
|
|
|
|
|
f"insert into user ({column_sql}) values ({placeholders})",
|
|
|
|
|
[values[name] for name in copied_columns],
|
|
|
|
|
)
|
|
|
|
|
verified = channel.execute(
|
|
|
|
|
"select id, is_active, is_admin from user where lower_name = ?",
|
|
|
|
|
(OWNER_NAME,),
|
|
|
|
|
).fetchall()
|
|
|
|
|
if len(verified) != 1 or verified[0][1:] != (1, 1):
|
|
|
|
|
raise RuntimeError("channel owner migration verification failed")
|
|
|
|
|
|
|
|
|
|
receipt.write_text(
|
|
|
|
|
json.dumps(
|
|
|
|
|
{
|
|
|
|
|
"schema": "guanghu.hlcc-owner-migration/v1",
|
|
|
|
|
"owner": OWNER_NAME,
|
|
|
|
|
"identity_only": True,
|
|
|
|
|
"password_hash_preserved": True,
|
|
|
|
|
"access_tokens_migrated": False,
|
|
|
|
|
"repositories_migrated": False,
|
|
|
|
|
"result": "VERIFIED",
|
|
|
|
|
},
|
|
|
|
|
ensure_ascii=False,
|
|
|
|
|
indent=2,
|
|
|
|
|
)
|
|
|
|
|
+ "\n",
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
receipt.chmod(0o600)
|
|
|
|
|
return "migrated"
|
|
|
|
|
finally:
|
|
|
|
|
channel.close()
|
|
|
|
|
legacy.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def api_json(
|
|
|
|
|
method: str,
|
|
|
|
|
path: str,
|
|
|
|
|
token: str = "",
|
|
|
|
|
body: dict[str, object] | None = None,
|
|
|
|
|
) -> tuple[int, dict[str, object]]:
|
|
|
|
|
headers = {"Accept": "application/json"}
|
|
|
|
|
data = None
|
|
|
|
|
if token:
|
|
|
|
|
headers["Authorization"] = f"token {token}"
|
|
|
|
|
if body is not None:
|
|
|
|
|
headers["Content-Type"] = "application/json"
|
|
|
|
|
data = json.dumps(body).encode("utf-8")
|
|
|
|
|
request = urllib.request.Request(
|
|
|
|
|
f"http://127.0.0.1:3340{path}",
|
|
|
|
|
data=data,
|
|
|
|
|
headers=headers,
|
|
|
|
|
method=method,
|
|
|
|
|
)
|
|
|
|
|
try:
|
|
|
|
|
with urllib.request.urlopen(request, timeout=15) as response:
|
|
|
|
|
raw = response.read()
|
|
|
|
|
return response.status, json.loads(raw or b"{}")
|
|
|
|
|
except urllib.error.HTTPError as error:
|
|
|
|
|
raw = error.read()
|
|
|
|
|
try:
|
|
|
|
|
payload = json.loads(raw or b"{}")
|
|
|
|
|
except json.JSONDecodeError:
|
|
|
|
|
payload = {}
|
|
|
|
|
return error.code, payload
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_bootstrap_token(channel_db: pathlib.Path, token_name: str) -> None:
|
|
|
|
|
connection = sqlite3.connect(channel_db, timeout=15)
|
|
|
|
|
try:
|
|
|
|
|
with connection:
|
|
|
|
|
owner = connection.execute(
|
|
|
|
|
"select id from user where lower_name = ?",
|
|
|
|
|
(OWNER_NAME,),
|
|
|
|
|
).fetchone()
|
|
|
|
|
if owner:
|
|
|
|
|
connection.execute(
|
|
|
|
|
"delete from access_token where uid = ? and name = ?",
|
|
|
|
|
(owner[0], token_name),
|
|
|
|
|
)
|
|
|
|
|
finally:
|
|
|
|
|
connection.close()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def generate_bootstrap_token(binary: pathlib.Path, token_name: str) -> str:
|
|
|
|
|
channel_db = STATE_ROOT / "data" / "hlcc.db"
|
|
|
|
|
delete_bootstrap_token(channel_db, token_name)
|
|
|
|
|
result = subprocess.run(
|
|
|
|
|
[
|
|
|
|
|
str(binary),
|
|
|
|
|
"admin",
|
|
|
|
|
"user",
|
|
|
|
|
"generate-access-token",
|
|
|
|
|
"--username",
|
|
|
|
|
OWNER_NAME,
|
|
|
|
|
"--token-name",
|
|
|
|
|
token_name,
|
|
|
|
|
"--scopes",
|
|
|
|
|
"write:repository",
|
|
|
|
|
"--raw",
|
|
|
|
|
"--config",
|
|
|
|
|
str(STATE_ROOT / "config" / "app.ini"),
|
|
|
|
|
"--work-path",
|
|
|
|
|
str(STATE_ROOT / "data"),
|
|
|
|
|
],
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
)
|
|
|
|
|
token = result.stdout.strip().splitlines()[-1]
|
|
|
|
|
if not re.fullmatch(r"[A-Za-z0-9_-]{32,160}", token):
|
|
|
|
|
raise RuntimeError("bootstrap access token format invalid")
|
|
|
|
|
return token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def seed_fifth_domain_channel(binary: pathlib.Path) -> str:
|
|
|
|
|
status, repository = api_json("GET", f"/api/v1/repos/{OWNER_NAME}/{CHANNEL_REPOSITORY}")
|
|
|
|
|
if status == 200:
|
|
|
|
|
if repository.get("private") is not False:
|
|
|
|
|
raise RuntimeError("existing channel repository is not public")
|
|
|
|
|
if repository.get("empty") is False and repository.get("default_branch") == "main":
|
|
|
|
|
return "already-present"
|
|
|
|
|
if repository.get("empty") is not True:
|
|
|
|
|
raise RuntimeError("existing channel repository state invalid")
|
|
|
|
|
elif status != 404:
|
|
|
|
|
raise RuntimeError("channel repository lookup failed")
|
|
|
|
|
|
|
|
|
|
token_name = "hlcc-fifth-domain-bootstrap"
|
|
|
|
|
token = generate_bootstrap_token(binary, token_name)
|
|
|
|
|
channel_db = STATE_ROOT / "data" / "hlcc.db"
|
|
|
|
|
try:
|
|
|
|
|
if status == 404:
|
|
|
|
|
created_status, _created = api_json(
|
|
|
|
|
"POST",
|
|
|
|
|
"/api/v1/user/repos",
|
|
|
|
|
token,
|
|
|
|
|
{
|
|
|
|
|
"name": CHANNEL_REPOSITORY,
|
|
|
|
|
"description": "光湖代码频道 · 冰朔第五域个人子频道 · 2026-07-23 新起点",
|
|
|
|
|
"private": False,
|
|
|
|
|
"auto_init": False,
|
|
|
|
|
"default_branch": "main",
|
|
|
|
|
},
|
|
|
|
|
)
|
|
|
|
|
if created_status != 201:
|
|
|
|
|
raise RuntimeError("channel repository creation failed")
|
|
|
|
|
|
|
|
|
|
source_commit = immutable_source_commit()
|
|
|
|
|
with tempfile.TemporaryDirectory(prefix="hlcc-seed-") as temporary:
|
|
|
|
|
root = pathlib.Path(temporary)
|
|
|
|
|
snapshot = root / "snapshot"
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["git", "clone", "--depth=1", "--branch", "main", LEGACY_REPOSITORY_URL, str(snapshot)],
|
|
|
|
|
check=True,
|
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
)
|
|
|
|
|
actual_commit = subprocess.run(
|
|
|
|
|
["git", "-C", str(snapshot), "rev-parse", "HEAD"],
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
text=True,
|
|
|
|
|
).stdout.strip()
|
|
|
|
|
if actual_commit != source_commit:
|
|
|
|
|
raise RuntimeError("legacy snapshot commit mismatch")
|
|
|
|
|
|
|
|
|
|
shutil.rmtree(snapshot / ".git")
|
|
|
|
|
subprocess.run(["git", "-C", str(snapshot), "init", "-b", "main"], check=True, capture_output=True)
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["git", "-C", str(snapshot), "config", "user.name", "光湖代码频道 · 铸渊"],
|
|
|
|
|
check=True,
|
|
|
|
|
)
|
|
|
|
|
subprocess.run(
|
|
|
|
|
["git", "-C", str(snapshot), "config", "user.email", "hlcc@guanghulab.invalid"],
|
|
|
|
|
check=True,
|
|
|
|
|
)
|
|
|
|
|
subprocess.run(["git", "-C", str(snapshot), "add", "-A"], check=True)
|
|
|
|
|
subprocess.run(
|
|
|
|
|
[
|
|
|
|
|
"git",
|
|
|
|
|
"-C",
|
|
|
|
|
str(snapshot),
|
|
|
|
|
"commit",
|
|
|
|
|
"-m",
|
|
|
|
|
(
|
|
|
|
|
f"[{SEED_COMMIT_NUMBER}][{SEED_CONTRIBUTION_NUMBER}] "
|
|
|
|
|
"feat: 以来光者贡献链启用冰朔第五域个人子频道"
|
|
|
|
|
),
|
|
|
|
|
],
|
|
|
|
|
check=True,
|
|
|
|
|
capture_output=True,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
credential = root / "credentials"
|
|
|
|
|
encoded = urllib.parse.quote(token, safe="")
|
|
|
|
|
credential.write_text(
|
|
|
|
|
f"http://{OWNER_NAME}:{encoded}@127.0.0.1:3340\n",
|
|
|
|
|
encoding="utf-8",
|
|
|
|
|
)
|
|
|
|
|
credential.chmod(0o600)
|
|
|
|
|
subprocess.run(
|
|
|
|
|
[
|
|
|
|
|
"git",
|
|
|
|
|
"-C",
|
|
|
|
|
str(snapshot),
|
|
|
|
|
"-c",
|
|
|
|
|
f"credential.helper=store --file {credential}",
|
|
|
|
|
"push",
|
2026-07-24 11:25:41 +08:00
|
|
|
"http://127.0.0.1:3340/bingshuo/guanghu-ice-heart.git",
|
2026-07-24 10:39:10 +08:00
|
|
|
"main:main",
|
|
|
|
|
],
|
|
|
|
|
check=True,
|
|
|
|
|
stdout=subprocess.DEVNULL,
|
|
|
|
|
stderr=subprocess.PIPE,
|
|
|
|
|
)
|
|
|
|
|
credential.unlink(missing_ok=True)
|
|
|
|
|
|
|
|
|
|
verified_status, verified = api_json(
|
|
|
|
|
"GET",
|
|
|
|
|
f"/api/v1/repos/{OWNER_NAME}/{CHANNEL_REPOSITORY}",
|
|
|
|
|
)
|
|
|
|
|
if (
|
|
|
|
|
verified_status != 200
|
|
|
|
|
or verified.get("private") is not False
|
|
|
|
|
or verified.get("empty") is not False
|
|
|
|
|
or verified.get("default_branch") != "main"
|
|
|
|
|
):
|
|
|
|
|
raise RuntimeError("channel repository verification failed")
|
|
|
|
|
return "seeded"
|
|
|
|
|
finally:
|
|
|
|
|
delete_bootstrap_token(channel_db, token_name)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def bootstrap() -> None:
|
|
|
|
|
process: subprocess.Popen[bytes] | None = None
|
|
|
|
|
try:
|
|
|
|
|
for directory in ("config", "data", "logs", "release", "tmp"):
|
|
|
|
|
(STATE_ROOT / directory).mkdir(parents=True, exist_ok=True)
|
|
|
|
|
set_status(stage="downloading")
|
|
|
|
|
files = {name: download_verified(name) for name in EXPECTED}
|
|
|
|
|
set_status(stage="verifying")
|
|
|
|
|
verify_release(files)
|
|
|
|
|
binary = files[BINARY_NAME]
|
|
|
|
|
binary.chmod(0o755)
|
|
|
|
|
config_source = pathlib.Path(__file__).with_name("app.ini")
|
|
|
|
|
config_target = STATE_ROOT / "config" / "app.ini"
|
|
|
|
|
shutil.copyfile(config_source, config_target)
|
|
|
|
|
config_target.chmod(0o600)
|
2026-07-24 10:54:14 +08:00
|
|
|
custom_source = pathlib.Path(__file__).with_name("custom")
|
|
|
|
|
custom_target = STATE_ROOT / "data" / "custom"
|
|
|
|
|
if custom_source.is_dir():
|
|
|
|
|
shutil.copytree(custom_source, custom_target, dirs_exist_ok=True)
|
2026-07-24 10:39:10 +08:00
|
|
|
set_status(stage="launching")
|
|
|
|
|
log_path = STATE_ROOT / "logs" / "hlcc.log"
|
|
|
|
|
log_handle = log_path.open("ab", buffering=0)
|
|
|
|
|
process = subprocess.Popen(
|
|
|
|
|
[
|
|
|
|
|
str(binary),
|
|
|
|
|
"web",
|
|
|
|
|
"--work-path",
|
|
|
|
|
str(STATE_ROOT / "data"),
|
|
|
|
|
"--config",
|
|
|
|
|
str(config_target),
|
|
|
|
|
],
|
|
|
|
|
stdout=log_handle,
|
|
|
|
|
stderr=subprocess.STDOUT,
|
|
|
|
|
)
|
|
|
|
|
wait_for_candidate(process)
|
|
|
|
|
set_status(stage="migrating-owner")
|
|
|
|
|
migrate_owner_identity()
|
|
|
|
|
set_status(stage="seeding-fifth-domain-channel")
|
|
|
|
|
seed_fifth_domain_channel(binary)
|
|
|
|
|
set_status(mode="isolated-candidate", ready=True, stage="ready")
|
|
|
|
|
return_code = process.wait()
|
|
|
|
|
raise RuntimeError(f"candidate stopped with code {return_code}")
|
|
|
|
|
except Exception as error:
|
|
|
|
|
if process and process.poll() is None:
|
|
|
|
|
process.terminate()
|
|
|
|
|
set_status(ok=False, ready=False, stage="failed", error=str(error)[:180])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def main() -> None:
|
|
|
|
|
thread = threading.Thread(target=bootstrap, name="hlcc-bootstrap", daemon=True)
|
|
|
|
|
thread.start()
|
|
|
|
|
server = http.server.ThreadingHTTPServer(("127.0.0.1", 3341), HealthHandler)
|
|
|
|
|
server.serve_forever()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
|
main()
|