同步移动端工程、设备控制改进与发布资料

This commit is contained in:
2026-09-26 19:21:12 +08:00
parent 912ba432cf
commit 98eadc5c8b
137 changed files with 9274 additions and 85 deletions
@@ -32,6 +32,8 @@ RUNTIME_GUARD_BODY = b'# Managed by the OTA component transaction\n[Service]\nRu
SERVICE = 'matrix-screen-controller.service'
WORKER = 'matrix-screen-controller-ota.service'
FRP = 'matrix-screen-frpc.service'
MOBILE_SERVICES = ('aw859-bluetooth.service', 'bluetooth.service')
MOBILE_FILES = ('bluetooth-conf', 'bluetooth-original', 'bluetooth-dropin', 'aw859-dropin')
JOURNAL = Path('ota/component-transaction')
FILES = {
'frpc': Path('/usr/local/bin/frpc'),
@@ -39,6 +41,10 @@ FILES = {
'frpc-dropin': Path('/etc/systemd/system/matrix-screen-frpc.service.d/user.conf'),
'app-unit': Path('/etc/systemd/system/matrix-screen-controller.service'),
'ota-unit': Path('/etc/systemd/system/matrix-screen-controller-ota.service'),
'bluetooth-conf': Path('/etc/bluetooth/main.conf'),
'bluetooth-original': Path('/etc/bluetooth/main.conf.matrix-mobile-original'),
'bluetooth-dropin': Path('/etc/systemd/system/bluetooth.service.d/50-matrix-mobile.conf'),
'aw859-dropin': Path('/etc/systemd/system/aw859-bluetooth.service.d/50-matrix-mobile.conf'),
}
@@ -229,15 +235,18 @@ def begin(source: Path, candidate: Path):
if RUNTIME_GUARD.exists():
raise RuntimeError('存在未清理的 OTA 运行目录保护,请先恢复')
bundle = source / 'system-dependencies/frpc'
mobile_installer = source / 'scripts/install_mobile_bluetooth.sh'
if not bundle.is_dir():
check_installed(source, version)
return
if not mobile_installer.is_file():
return
# Verify the pinned binary, not just a self-reported checksum file.
from app.ota.policy import required_components
expected = required_components(source, SoftwareVersion.parse(version))['frpc']
if hashlib.sha256((bundle / 'frpc').read_bytes()).hexdigest() != expected['sha256']:
raise RuntimeError('frpc payload differs from the registered dependency')
user = account()
if bundle.is_dir():
expected = required_components(source, SoftwareVersion.parse(version))['frpc']
if hashlib.sha256((bundle / 'frpc').read_bytes()).hexdigest() != expected['sha256']:
raise RuntimeError('frpc payload differs from the registered dependency')
user = account() if bundle.is_dir() else None
for root in (DATA, candidate):
if (root / JOURNAL).exists():
raise RuntimeError('存在未完成组件事务,请先恢复')
@@ -249,6 +258,13 @@ def begin(source: Path, candidate: Path):
'permissions': permission_snapshot(DATA), 'files': {},
'active': run(['systemctl', 'is-active', '--quiet', FRP], check=False).returncode == 0,
'enabled': run(['systemctl', 'is-enabled', '--quiet', FRP], check=False).returncode == 0}
if mobile_installer.is_file():
record['mobile_services'] = {
unit: {
'enabled_state': run(['systemctl', 'is-enabled', unit], check=False).stdout.decode().strip(),
'active': run(['systemctl', 'is-active', '--quiet', unit], check=False).returncode == 0,
} for unit in MOBILE_SERVICES
}
try:
for name, path in FILES.items():
record['files'][name] = metadata(path) if path.exists() else None
@@ -273,11 +289,14 @@ def begin(source: Path, candidate: Path):
'/usr/bin/python3', str(HELPER), 'watch'])
protect_runtime()
env = os.environ.copy()
env.update(FRPC_BUNDLE=str(bundle), FRPC_RUN_USER=user)
run(['/bin/sh', str(source / 'scripts/install_frpc_system.sh')], env=env)
if bundle.is_dir():
env.update(FRPC_BUNDLE=str(bundle), FRPC_RUN_USER=user)
run(['/bin/sh', str(source / 'scripts/install_frpc_system.sh')], env=env)
if mobile_installer.is_file():
run(['/bin/sh', str(mobile_installer)], env=env)
mirror_permissions(candidate)
check_installed(source, version)
print('FRP component transaction prepared; waiting for OTA health result')
print('System component transaction prepared; waiting for OTA health result')
except BaseException:
# The watcher handles subsequent worker failure. Restore immediately too,
# so a failed migration never leaves new system files while rolling back.
@@ -289,6 +308,10 @@ def begin(source: Path, candidate: Path):
def restore_components(journal: Path, record: dict):
mobile_services = record.get('mobile_services', {})
for unit in reversed(MOBILE_SERVICES):
if unit in mobile_services:
run(['systemctl', 'stop', unit], check=False)
run(['systemctl', 'stop', FRP], check=False)
for name in ('frpc', 'frpc-unit', 'frpc-dropin'):
path = FILES[name]
@@ -299,6 +322,15 @@ def restore_components(journal: Path, record: dict):
atomic(path, (journal / name).read_bytes(), item['mode'])
apply_metadata(path, item)
restore_permissions(DATA, record['permissions'])
if mobile_services:
for name in MOBILE_FILES:
path = FILES[name]
item = record['files'][name]
if item is None:
path.unlink(missing_ok=True)
else:
atomic(path, (journal / name).read_bytes(), item['mode'])
apply_metadata(path, item)
run(['systemctl', 'daemon-reload'])
# A previously absent unit cannot be disabled; avoid treating absence as error.
if record['files']['frpc-unit'] is not None:
@@ -309,6 +341,24 @@ def restore_components(journal: Path, record: dict):
link.unlink(missing_ok=True)
if record['active']:
run(['systemctl', 'start', FRP])
for unit in MOBILE_SERVICES:
previous = mobile_services.get(unit)
if previous is None:
continue
run(['systemctl', 'unmask', unit])
state = previous['enabled_state']
run(['systemctl', 'disable', unit], check=False)
if state == 'enabled':
run(['systemctl', 'enable', unit])
elif state == 'enabled-runtime':
run(['systemctl', 'enable', '--runtime', unit])
elif state in ('masked', 'masked-runtime'):
args = ['systemctl', 'mask']
if state == 'masked-runtime':
args.append('--runtime')
run(args + [unit])
if previous['active']:
run(['systemctl', 'start', unit])
def locate_journal():