"""Explicit development deployment; preserves board data and keeps a rollback copy.""" from __future__ import annotations import argparse import hashlib import json from pathlib import Path import tarfile import tempfile from uuid import uuid4 from remote_support import ROOT, connect, execute, redact FILES = [ 'app/main.py', 'app/control.py', 'app/network/manager.py', 'app/network/service.py', 'app/network/store.py', 'app/static/index.html', 'app/static/views/device.js', 'app/static/ui-copy.json', 'scripts/dedicated_host.py', 'scripts/deploy_walnutpi.sh', 'scripts/install_mobile_bluetooth.sh', 'scripts/ota_components.py', 'systemd/matrix-screen-controller.service', 'requirements.txt', 'FEATURE_UPDATED_AT', ] def main(): parser = argparse.ArgumentParser() parser.add_argument('--deploy', action='store_true', required=True) parser.add_argument('--test-rollback', action='store_true', help='Intentionally fail after installation and verify restoration on the real system') args = parser.parse_args() client, fields = connect() token = uuid4().hex[:12] stage = '/tmp/qms-mobile-stage-' + token backup = '/opt/matrix-screen-controller.mobile-backup-' + token source = ROOT / '核桃派软件源代码' files = FILES + [p.relative_to(source).as_posix() for p in (source / 'app/mobile').glob('*.py')] def snapshot(): paths = ['/opt/matrix-screen-controller/' + name for name in files] paths += ['/var/lib/matrix-screen-controller/' + name for name in ('config.json', 'wifi_config.json', 'mobile/identity.json')] import shlex program = ('import hashlib,json;from pathlib import Path;' 'print(json.dumps({p:hashlib.sha256(Path(p).read_bytes()).hexdigest() if Path(p).is_file() else None for p in ' + repr(paths) + '}))') code, output, _ = execute(client, 'python3 -c ' + shlex.quote(program), password=fields['密码']) if code: raise RuntimeError('Cannot capture rollback verification snapshot') return json.loads(output) before = snapshot() if args.test_rollback else None dependencies = ROOT / '发布更新相关/其他依赖/aarch64-py311' try: with tempfile.TemporaryDirectory(prefix='qms-deploy-') as local: bundle = Path(local) / 'payload.tar.gz' with tarfile.open(bundle, 'w:gz') as archive: for name in files: archive.add(source / name, arcname='source/' + name) for pattern in ('cryptography-46.0.5-*.whl', 'dbus_next-0.2.3-*.whl', 'cffi-2.1.1-*.whl', 'pycparser-3.0-*.whl'): matches = list(dependencies.glob(pattern)) if len(matches) != 1: raise RuntimeError('Missing or ambiguous offline wheel') archive.add(matches[0], arcname='wheels/' + matches[0].name) digest = hashlib.sha256(bundle.read_bytes()).hexdigest() sftp = client.open_sftp() sftp.mkdir(stage) sftp.put(str(bundle), stage + '/payload.tar.gz') sftp.close() script = r''' set -eu STAGE=__STAGE__ BACKUP=__BACKUP__ TARGET=/opt/matrix-screen-controller test -d "$TARGET/.venv" test ! -e "$BACKUP" printf '%s %s\n' '__DIGEST__' "$STAGE/payload.tar.gz" | sha256sum -c - tar -xzf "$STAGE/payload.tar.gz" -C "$STAGE" mkdir "$BACKUP" cp -a "$TARGET/." "$BACKUP/" test -x "$BACKUP/.venv/bin/python" cp -a /etc/systemd/system/matrix-screen-controller.service "$BACKUP/original-systemd.service" cp -a /etc/bluetooth/main.conf "$BACKUP/original-bluetooth.conf" systemctl is-enabled bluetooth.service > "$BACKUP/bluetooth.enabled" || true systemctl is-enabled aw859-bluetooth.service > "$BACKUP/aw859.enabled" || true for unit in bluetooth aw859-bluetooth; do config=/etc/systemd/system/$unit.service.d/50-matrix-mobile.conf if test -f "$config"; then cp -a "$config" "$BACKUP/$unit.dropin"; fi done rollback() { set +e systemctl stop matrix-screen-controller.service || true if test -d "$TARGET/.venv" && test ! -e "$STAGE/failed-venv"; then mv "$TARGET/.venv" "$STAGE/failed-venv" cp -a "$BACKUP/.venv" "$TARGET/.venv" fi cp -a "$BACKUP/app/." "$TARGET/app/" cp -a "$BACKUP/scripts/." "$TARGET/scripts/" cp -a "$BACKUP/systemd/." "$TARGET/systemd/" cp -a "$BACKUP/requirements.txt" "$TARGET/requirements.txt" cp -a "$BACKUP/FEATURE_UPDATED_AT" "$TARGET/FEATURE_UPDATED_AT" cp -a "$BACKUP/original-systemd.service" /etc/systemd/system/matrix-screen-controller.service cp -a "$BACKUP/original-bluetooth.conf" /etc/bluetooth/main.conf for unit in bluetooth aw859-bluetooth; do config=/etc/systemd/system/$unit.service.d/50-matrix-mobile.conf if test -f "$BACKUP/$unit.dropin"; then cp -a "$BACKUP/$unit.dropin" "$config"; else rm -f -- "$config"; fi done systemctl daemon-reload if ! grep -qx enabled "$BACKUP/bluetooth.enabled"; then systemctl disable --now bluetooth.service || true; fi if ! grep -qx enabled "$BACKUP/aw859.enabled"; then systemctl disable --now aw859-bluetooth.service || true; fi systemctl start matrix-screen-controller.service || true } COMMITTED=0 trap 'if test "$COMMITTED" = 0; then rollback; fi' EXIT trap 'exit 1' HUP INT TERM "$TARGET/.venv/bin/python" -m pip install --no-index --find-links "$STAGE/wheels" cryptography==46.0.5 dbus-next==0.2.3 systemctl stop matrix-screen-controller.service cp -a "$STAGE/source/." "$TARGET/" /bin/sh "$TARGET/scripts/install_mobile_bluetooth.sh" install -m 0644 "$TARGET/systemd/matrix-screen-controller.service" /etc/systemd/system/matrix-screen-controller.service systemctl daemon-reload __FAULT_INJECTION__ systemctl start matrix-screen-controller.service attempt=0 while test "$attempt" -lt 25; do if curl --fail --silent http://127.0.0.1:8080/api/status > "$STAGE/status.json"; then python3 - "$STAGE/status.json" <<'PY' import json, sys status = json.load(open(sys.argv[1], encoding='utf-8')) print(json.dumps({'service_active': True, 'mobile_available': status.get('mobile', {}).get('available'), 'mobile_reason': status.get('mobile', {}).get('reason')}, ensure_ascii=False)) PY echo "Rollback copy retained: $BACKUP" COMMITTED=1 trap - EXIT HUP INT TERM exit 0 fi attempt=$((attempt + 1)) sleep 1 done exit 1 '''.replace('__STAGE__', stage).replace('__BACKUP__', backup).replace('__DIGEST__', digest).replace('__FAULT_INJECTION__', 'echo "Intentional rollback test failure"; exit 97' if args.test_rollback else ':') sftp = client.open_sftp() with sftp.open(stage + '/deploy.sh', 'w') as stream: stream.write(script) sftp.close() code, output, error = execute(client, '/bin/sh ' + stage + '/deploy.sh', password=fields['密码'], timeout=240) print(redact(output, fields)) # Raw package errors do not contain credentials; still redact known private fields. print(redact(error, fields)) if args.test_rollback: if code != 97 or snapshot() != before: raise RuntimeError('Rollback verification failed; retained backup requires inspection') code, output, _ = execute(client, 'systemctl is-active matrix-screen-controller.service', timeout=15) if code or output.strip() != 'active': raise RuntimeError('Rollback restored files but service is not active') print('Intentional failure rolled back: program and persistent file digests unchanged; real service active.') return if code: raise RuntimeError('Deployment failed; inspect retained rollback copy') print('Development deployment finished; BLE validation is still required.') finally: client.close() if __name__ == '__main__': main()