Files

159 lines
7.7 KiB
Python

"""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()