mirror of
https://github.com/firestar5683/StarPilot.git
synced 2026-07-17 07:12:08 +08:00
rollback
This commit is contained in:
@@ -146,6 +146,10 @@
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.updateRollbackSection {
|
||||
margin-top: var(--margin-base);
|
||||
}
|
||||
|
||||
.updateAdvancedNote {
|
||||
font-size: var(--font-size-sm);
|
||||
margin-top: var(--margin-sm);
|
||||
@@ -165,6 +169,15 @@
|
||||
margin-top: var(--margin-sm);
|
||||
}
|
||||
|
||||
.updateRollbackMeta {
|
||||
margin-top: var(--margin-sm);
|
||||
}
|
||||
|
||||
.updateRollbackMeta p {
|
||||
font-size: var(--font-size-sm);
|
||||
margin: 0 0 var(--margin-xs) 0;
|
||||
}
|
||||
|
||||
.updateSelect {
|
||||
background-color: var(--main-bg);
|
||||
border: 1px solid var(--track-color);
|
||||
|
||||
@@ -15,6 +15,7 @@ const state = reactive({
|
||||
branchesError: "",
|
||||
branchesBusy: false,
|
||||
switchBusy: false,
|
||||
rollbackBusy: false,
|
||||
})
|
||||
|
||||
let initialized = false
|
||||
@@ -104,6 +105,27 @@ function activeCommitsLabel() {
|
||||
: "View latest commit for this branch"
|
||||
}
|
||||
|
||||
function formatRecordedAt(value) {
|
||||
const text = String(value || "").trim()
|
||||
if (!text) return "Unknown"
|
||||
|
||||
const parsed = new Date(text)
|
||||
if (Number.isNaN(parsed.getTime())) return text
|
||||
return parsed.toLocaleString()
|
||||
}
|
||||
|
||||
function rollbackTargetSummary() {
|
||||
const branch = String(state.status?.rollbackBranch || "").trim()
|
||||
const commit = String(state.status?.rollbackCommit || "").trim()
|
||||
if (!branch && !commit) return "No previous installed version recorded yet."
|
||||
if (branch && commit) return `${branch} / ${shortHash(commit)}`
|
||||
return branch || shortHash(commit)
|
||||
}
|
||||
|
||||
function hasRecordedRollbackTarget() {
|
||||
return !!String(state.status?.rollbackCommit || "").trim()
|
||||
}
|
||||
|
||||
function shouldShowPrimaryUpdateAction() {
|
||||
if (state.status?.running) return true
|
||||
if (isSelectedBranchDifferent()) return true
|
||||
@@ -468,6 +490,70 @@ async function runBranchSwitch() {
|
||||
}
|
||||
}
|
||||
|
||||
async function runRollback() {
|
||||
if (state.rollbackBusy) return
|
||||
if (state.status?.running) {
|
||||
showSnackbar("An update action is already running.")
|
||||
return
|
||||
}
|
||||
if (state.status?.isOnroad) {
|
||||
showSnackbar("Actions are blocked while onroad.", "error")
|
||||
return
|
||||
}
|
||||
|
||||
const rollbackCommit = String(state.status?.rollbackCommit || "").trim()
|
||||
const rollbackBranch = String(state.status?.rollbackBranch || "").trim()
|
||||
if (!rollbackCommit || !state.status?.rollbackAvailable) {
|
||||
showSnackbar("No previous installed version is available to roll back to.", "error")
|
||||
return
|
||||
}
|
||||
|
||||
const confirmed = window.confirm(
|
||||
"Roll back to the previous installed version?\n\n" +
|
||||
`Target: ${rollbackBranch || "Unknown"} @ ${shortHash(rollbackCommit)}\n\n` +
|
||||
"- This restores the version this device was running before the last Galaxy update.\n" +
|
||||
"- Automatic updates will be turned off.\n" +
|
||||
"- Your device will reboot when the rollback is done.\n\n" +
|
||||
"Continue?"
|
||||
)
|
||||
if (!confirmed) return
|
||||
|
||||
state.rollbackBusy = true
|
||||
try {
|
||||
reconnectPending = false
|
||||
rebootNoticeShown = false
|
||||
const response = await fetch("/api/update/rollback", { method: "POST" })
|
||||
const payload = await readJsonPayload(response)
|
||||
if (!response.ok) {
|
||||
throw new Error(payload.error || response.statusText || "Failed to start rollback")
|
||||
}
|
||||
|
||||
state.status = {
|
||||
...(state.status || {}),
|
||||
running: true,
|
||||
stage: "starting",
|
||||
progressStep: 1,
|
||||
progressTotalSteps: state.status?.progressTotalSteps || 5,
|
||||
progressStepPercent: 0,
|
||||
progressPercent: 0,
|
||||
progressLabel: "Preparing rollback",
|
||||
progressDetail: "Initializing rollback...",
|
||||
message: payload.message || "Rollback started. Device will reboot when complete.",
|
||||
lastError: "",
|
||||
lastMode: "rollback",
|
||||
automaticUpdates: false,
|
||||
}
|
||||
state.error = ""
|
||||
showSnackbar(payload.message || "Rollback started.")
|
||||
await fetchStatus(false)
|
||||
ensurePolling()
|
||||
} catch (error) {
|
||||
showSnackbar(error?.message || "Failed to start rollback", "error")
|
||||
} finally {
|
||||
state.rollbackBusy = false
|
||||
}
|
||||
}
|
||||
|
||||
function initialize() {
|
||||
if (initialized) return
|
||||
initialized = true
|
||||
@@ -577,6 +663,32 @@ export function UpdateManager() {
|
||||
</div>
|
||||
${() => state.branchesError ? html`<p class="updateError"><strong>Branch List:</strong> ${state.branchesError}</p>` : ""}
|
||||
</div>
|
||||
|
||||
<div class="updateRollbackSection">
|
||||
<div class="updateBranchHeader">
|
||||
<strong>Rollback</strong>
|
||||
</div>
|
||||
<p class="updateAdvancedNote">
|
||||
Restores the previous installed version recorded before the last Galaxy update on this device.
|
||||
</p>
|
||||
<div class="updateRollbackMeta">
|
||||
<p><strong>Previous Installed Version:</strong> ${() => rollbackTargetSummary()}</p>
|
||||
<p><strong>Saved:</strong> ${() => formatRecordedAt(state.status?.rollbackRecordedAt)}</p>
|
||||
</div>
|
||||
<button
|
||||
class="updateButton danger"
|
||||
disabled="${() => !!state.status?.isOnroad || !!state.status?.running || state.rollbackBusy || !state.status?.rollbackAvailable || false}"
|
||||
@click="${() => runRollback()}">
|
||||
${() => (state.status?.running && state.status?.lastMode === "rollback")
|
||||
? "Rolling Back..."
|
||||
: (state.rollbackBusy ? "Starting..." : "Roll Back to Previous Version")}
|
||||
</button>
|
||||
${() => !state.status?.rollbackAvailable
|
||||
? html`<p class="updateHint">${hasRecordedRollbackTarget()
|
||||
? "Current install already matches the saved previous version."
|
||||
: "A rollback target appears here after the next successful Galaxy update or branch switch."}</p>`
|
||||
: html`<p class="updateHint">Rollback disables automatic updates so the device does not immediately update forward again.</p>`}
|
||||
</div>
|
||||
` : ""}
|
||||
|
||||
${() => !isFactoryResetStatusActive() && state.status?.message && state.status?.stage !== "rebooting" ? html`<p class="updateMessage">${state.status.message}</p>` : ""}
|
||||
|
||||
@@ -547,9 +547,13 @@ _FAST_UPDATE_PROGRESS_UPDATE_INTERVAL_S = 5.0
|
||||
_FAST_UPDATE_REBOOT_NOTICE_SECONDS = 6.0
|
||||
_FAST_UPDATE_FETCH_TIMEOUT_S = 60
|
||||
_FAST_BRANCH_SWITCH_FETCH_TIMEOUT_S = 60
|
||||
_FAST_ROLLBACK_FETCH_TIMEOUT_S = 60
|
||||
_FACTORY_RESET_DELETE_TIMEOUT_S = 1800
|
||||
_GIT_PROGRESS_PERCENT_RE = re.compile(r'([A-Za-z][A-Za-z /_-]+):\s*([0-9]{1,3})%')
|
||||
_GIT_SUBMODULE_SECTION_RE = re.compile(r'^\s*\[submodule\s+"[^"]+"\]\s*$', re.MULTILINE)
|
||||
_ROLLBACK_REF = "refs/starpilot/rollback"
|
||||
_ROLLBACK_BRANCH_CONFIG_KEY = "starpilot.rollbackbranch"
|
||||
_ROLLBACK_RECORDED_AT_CONFIG_KEY = "starpilot.rollbackrecordedat"
|
||||
_TESTING_GROUNDS_SCHEMA_VERSION = SHARED_TESTING_GROUNDS_SCHEMA_VERSION
|
||||
_TESTING_GROUNDS_SLOT_COUNT = len(SHARED_TESTING_GROUNDS_SLOT_DEFINITIONS)
|
||||
_TESTING_GROUNDS_DEFAULT_VARIANT = SHARED_DEFAULT_TESTING_GROUND_VARIANT
|
||||
@@ -1121,6 +1125,18 @@ def _build_shallow_fetch_args(branch):
|
||||
branch,
|
||||
]
|
||||
|
||||
def _build_shallow_fetch_commit_args(commit):
|
||||
return [
|
||||
"-c", "gc.auto=0",
|
||||
"-c", "maintenance.auto=false",
|
||||
"fetch",
|
||||
"--progress",
|
||||
"--depth=1",
|
||||
"--no-recurse-submodules",
|
||||
"origin",
|
||||
commit,
|
||||
]
|
||||
|
||||
def _run_git_with_progress(repo_path, args, timeout, step, label):
|
||||
cmd = [*_git_base_cmd(), *args]
|
||||
|
||||
@@ -1295,6 +1311,83 @@ def _git_stdout(repo_path, args, timeout=15):
|
||||
raise RuntimeError(stderr)
|
||||
return (result.stdout or "").strip()
|
||||
|
||||
def _git_config_get(repo_path, key):
|
||||
try:
|
||||
return _git_stdout(repo_path, ["config", "--local", "--get", key], timeout=10)
|
||||
except Exception:
|
||||
return ""
|
||||
|
||||
def _git_config_set(repo_path, key, value):
|
||||
result = _run_git(repo_path, ["config", "--local", "--replace-all", key, str(value)], timeout=15)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError((result.stderr or result.stdout or f"git config failed for {key}").strip())
|
||||
|
||||
def _git_config_unset(repo_path, key):
|
||||
result = _run_git(repo_path, ["config", "--local", "--unset-all", key], timeout=15)
|
||||
if result.returncode not in (0, 5):
|
||||
raise RuntimeError((result.stderr or result.stdout or f"git config unset failed for {key}").strip())
|
||||
|
||||
def _git_update_ref(repo_path, ref_name, commit):
|
||||
result = _run_git(repo_path, ["update-ref", ref_name, commit], timeout=15)
|
||||
if result.returncode != 0:
|
||||
raise RuntimeError((result.stderr or result.stdout or f"git update-ref failed for {ref_name}").strip())
|
||||
|
||||
def _git_delete_ref(repo_path, ref_name):
|
||||
result = _run_git(repo_path, ["update-ref", "-d", ref_name], timeout=15)
|
||||
if result.returncode not in (0, 1):
|
||||
raise RuntimeError((result.stderr or result.stdout or f"git update-ref delete failed for {ref_name}").strip())
|
||||
|
||||
def _git_has_commit(repo_path, commit):
|
||||
result = _run_git(repo_path, ["cat-file", "-e", f"{commit}^{{commit}}"], timeout=15)
|
||||
return result.returncode == 0
|
||||
|
||||
def _save_rollback_target(repo_path, branch, commit):
|
||||
safe_commit = str(commit or "").strip()
|
||||
if not safe_commit:
|
||||
raise RuntimeError("Missing rollback commit")
|
||||
|
||||
safe_branch = str(branch or "").strip()
|
||||
if safe_branch and not _is_valid_git_branch_name(repo_path, safe_branch):
|
||||
raise RuntimeError(f"Invalid rollback branch '{safe_branch}'")
|
||||
|
||||
_git_update_ref(repo_path, _ROLLBACK_REF, safe_commit)
|
||||
if safe_branch:
|
||||
_git_config_set(repo_path, _ROLLBACK_BRANCH_CONFIG_KEY, safe_branch)
|
||||
else:
|
||||
_git_config_unset(repo_path, _ROLLBACK_BRANCH_CONFIG_KEY)
|
||||
_git_config_set(repo_path, _ROLLBACK_RECORDED_AT_CONFIG_KEY, datetime.now(timezone.utc).isoformat())
|
||||
|
||||
def _clear_rollback_target(repo_path):
|
||||
_git_delete_ref(repo_path, _ROLLBACK_REF)
|
||||
_git_config_unset(repo_path, _ROLLBACK_BRANCH_CONFIG_KEY)
|
||||
_git_config_unset(repo_path, _ROLLBACK_RECORDED_AT_CONFIG_KEY)
|
||||
|
||||
def _load_rollback_target(repo_path):
|
||||
commit = ""
|
||||
try:
|
||||
commit = _git_stdout(repo_path, ["rev-parse", "--verify", f"{_ROLLBACK_REF}^{{commit}}"], timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
branch = _git_config_get(repo_path, _ROLLBACK_BRANCH_CONFIG_KEY)
|
||||
recorded_at = _git_config_get(repo_path, _ROLLBACK_RECORDED_AT_CONFIG_KEY)
|
||||
|
||||
current_branch = ""
|
||||
current_commit = ""
|
||||
try:
|
||||
current_branch = _git_stdout(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"], timeout=10)
|
||||
current_commit = _git_stdout(repo_path, ["rev-parse", "HEAD"], timeout=10)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
available = bool(commit) and (commit != current_commit or (branch and branch != current_branch))
|
||||
return {
|
||||
"rollbackBranch": branch,
|
||||
"rollbackCommit": commit,
|
||||
"rollbackRecordedAt": recorded_at,
|
||||
"rollbackAvailable": available,
|
||||
}
|
||||
|
||||
def _is_valid_git_branch_name(repo_path, branch_name):
|
||||
branch = str(branch_name or "").strip()
|
||||
if not branch or branch.startswith("-") or "\x00" in branch:
|
||||
@@ -1463,6 +1556,7 @@ def _collect_fast_update_info(include_remote=True):
|
||||
remote_error = ""
|
||||
origin_remote = ""
|
||||
commits_url = ""
|
||||
rollback_data = _load_rollback_target(repo_path)
|
||||
|
||||
try:
|
||||
branch = _git_stdout(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
@@ -1481,6 +1575,7 @@ def _collect_fast_update_info(include_remote=True):
|
||||
"remoteError": str(exception),
|
||||
"originRemote": origin_remote,
|
||||
"commitsUrl": commits_url,
|
||||
**rollback_data,
|
||||
}
|
||||
|
||||
if origin_remote:
|
||||
@@ -1518,6 +1613,7 @@ def _collect_fast_update_info(include_remote=True):
|
||||
"remoteError": remote_error,
|
||||
"originRemote": origin_remote,
|
||||
"commitsUrl": commits_url,
|
||||
**rollback_data,
|
||||
}
|
||||
|
||||
def _fast_update_worker():
|
||||
@@ -1527,6 +1623,11 @@ def _fast_update_worker():
|
||||
try:
|
||||
_set_fast_update_progress(1, "Preparing update", 10.0, "Resolving active branch...")
|
||||
branch = _git_stdout(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
current_commit = _git_stdout(repo_path, ["rev-parse", "HEAD"])
|
||||
try:
|
||||
_save_rollback_target(repo_path, branch, current_commit)
|
||||
except Exception as exception:
|
||||
print(f"Fast update rollback target save failed: {exception}")
|
||||
_set_fast_update_progress(1, "Preparing update", 100.0, f"Branch: {branch}")
|
||||
_set_fast_update_state(
|
||||
running=True,
|
||||
@@ -1570,6 +1671,11 @@ def _branch_switch_worker(target_branch):
|
||||
try:
|
||||
_set_fast_update_progress(1, "Preparing branch switch", 10.0, f"Target: {target_branch}")
|
||||
current_branch = _git_stdout(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
current_commit = _git_stdout(repo_path, ["rev-parse", "HEAD"])
|
||||
try:
|
||||
_save_rollback_target(repo_path, current_branch, current_commit)
|
||||
except Exception as exception:
|
||||
print(f"Branch switch rollback target save failed: {exception}")
|
||||
_set_fast_update_progress(1, "Preparing branch switch", 100.0, f"{current_branch} -> {target_branch}")
|
||||
_set_fast_update_state(
|
||||
running=True,
|
||||
@@ -1612,6 +1718,78 @@ def _branch_switch_worker(target_branch):
|
||||
except Exception as exception:
|
||||
_set_fast_update_error_state("Fast branch switch failed.", exception)
|
||||
|
||||
def _rollback_worker():
|
||||
started_at = time.time()
|
||||
repo_path = str(_get_openpilot_root())
|
||||
|
||||
try:
|
||||
rollback_state = _load_rollback_target(repo_path)
|
||||
target_branch = str(rollback_state.get("rollbackBranch") or "").strip()
|
||||
target_commit = str(rollback_state.get("rollbackCommit") or "").strip()
|
||||
if not target_commit:
|
||||
raise RuntimeError("No previous installed version has been recorded yet.")
|
||||
|
||||
current_branch = _git_stdout(repo_path, ["rev-parse", "--abbrev-ref", "HEAD"])
|
||||
current_commit = _git_stdout(repo_path, ["rev-parse", "HEAD"])
|
||||
if target_commit == current_commit and (not target_branch or target_branch == current_branch):
|
||||
raise RuntimeError("Current install already matches the saved rollback target.")
|
||||
|
||||
if not target_branch:
|
||||
raise RuntimeError("Saved rollback branch is missing.")
|
||||
if not _is_valid_git_branch_name(repo_path, target_branch):
|
||||
raise RuntimeError(f"Saved rollback branch '{target_branch}' is invalid.")
|
||||
|
||||
short_commit = target_commit[:10]
|
||||
_set_fast_update_progress(1, "Preparing rollback", 10.0, f"Target: {target_branch} @ {short_commit}")
|
||||
_set_fast_update_state(
|
||||
running=True,
|
||||
stage="rolling-back",
|
||||
message=f"Rolling back to the previous installed version on '{target_branch}'...",
|
||||
lastError="",
|
||||
lastBranch=target_branch,
|
||||
lastMode="rollback",
|
||||
startedAt=started_at,
|
||||
finishedAt=0.0,
|
||||
)
|
||||
_set_fast_update_progress(1, "Preparing rollback", 100.0, f"{current_branch} -> {target_branch} @ {short_commit}")
|
||||
|
||||
if _git_has_commit(repo_path, target_commit):
|
||||
_set_fast_update_progress(2, "Resolving rollback target", 100.0, "Previous installed version is already available locally.")
|
||||
else:
|
||||
_set_fast_update_progress(2, "Resolving rollback target", 0.0, f"Fetching saved version {short_commit} from origin...")
|
||||
fetch_rc, fetch_output = _run_git_with_progress(
|
||||
repo_path,
|
||||
_build_shallow_fetch_commit_args(target_commit),
|
||||
timeout=_FAST_ROLLBACK_FETCH_TIMEOUT_S,
|
||||
step=2,
|
||||
label="Resolving rollback target",
|
||||
)
|
||||
if fetch_rc != 0 or not _git_has_commit(repo_path, target_commit):
|
||||
raise RuntimeError(fetch_output.strip() or f"Unable to fetch rollback commit {short_commit}")
|
||||
|
||||
_set_fast_update_progress(3, "Applying rollback target", 20.0, f"Checking out {target_branch} @ {short_commit}...")
|
||||
checkout = _run_git(repo_path, ["checkout", "--force", "-B", target_branch, target_commit], timeout=120)
|
||||
if checkout.returncode != 0:
|
||||
raise RuntimeError((checkout.stderr or checkout.stdout or "git checkout failed").strip())
|
||||
|
||||
reset = _run_git(repo_path, ["reset", "--hard", target_commit], timeout=120)
|
||||
if reset.returncode != 0:
|
||||
raise RuntimeError((reset.stderr or reset.stdout or "git reset failed").strip())
|
||||
|
||||
_run_git(repo_path, ["branch", "--set-upstream-to", f"origin/{target_branch}", target_branch], timeout=30)
|
||||
_set_fast_update_progress(3, "Applying rollback target", 100.0, f"Now on {target_branch} @ {short_commit}.")
|
||||
|
||||
_run_submodule_update_if_needed(repo_path, step=4)
|
||||
try:
|
||||
_clear_rollback_target(repo_path)
|
||||
except Exception as exception:
|
||||
print(f"Rollback target clear failed: {exception}")
|
||||
_finish_update_and_reboot(
|
||||
f"Rolled back to the previous installed version on '{target_branch}'. Automatic updates were disabled and the device is rebooting now."
|
||||
)
|
||||
except Exception as exception:
|
||||
_set_fast_update_error_state("Rollback failed.", exception)
|
||||
|
||||
def _get_openpilot_root():
|
||||
global _openpilot_root_cache
|
||||
if _openpilot_root_cache is not None:
|
||||
@@ -4882,6 +5060,52 @@ def setup(app):
|
||||
"warning": "Fast update skips backup creation and finalization safeguards.",
|
||||
}), 202
|
||||
|
||||
@app.route("/api/update/rollback", methods=["POST"])
|
||||
def run_update_rollback():
|
||||
if params.get_bool("IsOnroad"):
|
||||
return jsonify({"error": "Cannot roll back while driving."}), 409
|
||||
|
||||
repo_path = str(_get_openpilot_root())
|
||||
rollback_state = _load_rollback_target(repo_path)
|
||||
target_branch = str(rollback_state.get("rollbackBranch") or "").strip()
|
||||
target_commit = str(rollback_state.get("rollbackCommit") or "").strip()
|
||||
rollback_available = bool(rollback_state.get("rollbackAvailable"))
|
||||
|
||||
if not target_commit:
|
||||
return jsonify({"error": "No previous installed version has been recorded yet."}), 409
|
||||
if not target_branch:
|
||||
return jsonify({"error": "Saved rollback branch is missing."}), 409
|
||||
if not rollback_available:
|
||||
return jsonify({"error": "Current install already matches the saved previous version."}), 409
|
||||
|
||||
with _fast_update_lock:
|
||||
if _fast_update_state.get("running"):
|
||||
return jsonify({"error": "Another update action is already in progress."}), 409
|
||||
_fast_update_state.update({
|
||||
"running": True,
|
||||
"stage": "starting",
|
||||
"message": f"Starting rollback to '{target_branch}'...",
|
||||
"lastError": "",
|
||||
"lastBranch": target_branch,
|
||||
"lastMode": "rollback",
|
||||
"startedAt": time.time(),
|
||||
"finishedAt": 0.0,
|
||||
"progressStep": 1,
|
||||
"progressTotalSteps": _FAST_UPDATE_TOTAL_STEPS,
|
||||
"progressStepPercent": 0.0,
|
||||
"progressPercent": 0.0,
|
||||
"progressLabel": "Preparing rollback",
|
||||
"progressDetail": "Initializing rollback...",
|
||||
})
|
||||
|
||||
params.put_bool("AutomaticUpdates", False)
|
||||
threading.Thread(target=_rollback_worker, daemon=True).start()
|
||||
|
||||
return jsonify({
|
||||
"message": f"Rollback started for the previous installed version on '{target_branch}'. Automatic updates were disabled and the device will reboot when complete.",
|
||||
"warning": "Rollback restores the previously installed version recorded before the last Galaxy update.",
|
||||
}), 202
|
||||
|
||||
@app.route("/api/update/factory_reset", methods=["POST"])
|
||||
def run_factory_reset():
|
||||
if _safe_params_get_bool("IsOnroad"):
|
||||
|
||||
Reference in New Issue
Block a user