93 lines
2.0 KiB
Python
93 lines
2.0 KiB
Python
"""
|
|
Stable recovery boot stub.
|
|
|
|
Install once as /boot.py. It is deliberately excluded from normal OTA updates.
|
|
If a staged launcher/application update is interrupted or cannot boot, this
|
|
restores the previous .bak files.
|
|
"""
|
|
|
|
try:
|
|
import ujson as json
|
|
except ImportError:
|
|
import json
|
|
try:
|
|
import uos as os
|
|
except ImportError:
|
|
import os
|
|
|
|
PENDING_FILE = "/update_pending.json"
|
|
|
|
|
|
def exists(path):
|
|
try:
|
|
os.stat(path)
|
|
return True
|
|
except OSError:
|
|
return False
|
|
|
|
|
|
def remove(path):
|
|
try:
|
|
os.remove(path)
|
|
except OSError:
|
|
pass
|
|
|
|
|
|
def read_pending():
|
|
if not exists(PENDING_FILE):
|
|
return None
|
|
try:
|
|
with open(PENDING_FILE, "r") as f:
|
|
return json.loads(f.read())
|
|
except Exception:
|
|
return None
|
|
|
|
|
|
def rollback(pending):
|
|
if not pending:
|
|
return False
|
|
restored = False
|
|
for item in reversed(pending.get("files", [])):
|
|
target = item.get("target")
|
|
backup = item.get("backup")
|
|
if not target or not backup:
|
|
continue
|
|
try:
|
|
if exists(backup):
|
|
remove(target)
|
|
os.rename(backup, target)
|
|
restored = True
|
|
except Exception as exc:
|
|
print("[boot] rollback failed:", target, exc)
|
|
remove(PENDING_FILE)
|
|
return restored
|
|
|
|
|
|
pending = read_pending()
|
|
|
|
# Never boot a half-committed file set after a power loss.
|
|
if pending and not pending.get("commit_complete", False):
|
|
print("[boot] interrupted OTA commit; rolling back")
|
|
rollback(pending)
|
|
try:
|
|
import machine
|
|
machine.reset()
|
|
except Exception:
|
|
raise SystemExit
|
|
|
|
try:
|
|
import launcher
|
|
launcher.run()
|
|
except Exception as exc:
|
|
print("[boot] launcher/application startup failed:", exc)
|
|
pending = read_pending()
|
|
if pending:
|
|
print("[boot] reverting pending OTA")
|
|
rollback(pending)
|
|
try:
|
|
import machine
|
|
machine.reset()
|
|
except Exception:
|
|
pass
|
|
raise
|