diff --git a/selfdrive/manager/manager.py b/selfdrive/manager/manager.py index cc6cc8d..2b8bcff 100755 --- a/selfdrive/manager/manager.py +++ b/selfdrive/manager/manager.py @@ -4,7 +4,9 @@ import os import signal import subprocess import sys +import threading import traceback +from pathlib import Path from typing import List, Tuple, Union from cereal import log @@ -404,6 +406,35 @@ def manager_thread() -> None: if params_memory.get_bool("FrogPilotTogglesUpdated"): update_frogpilot_params(params, params_memory) +def backup_openpilot(): + # Configure the auto backup generator + backup_dir_path = '/data/backups' + Path(backup_dir_path).mkdir(parents=True, exist_ok=True) + + # Sort backups by creation time and only keep the 5 newest auto generated ones + auto_backups = sorted([f for f in os.listdir(backup_dir_path) if f.endswith("_auto")], + key=lambda x: os.path.getmtime(os.path.join(backup_dir_path, x))) + for old_backup in auto_backups: + subprocess.run(['sudo', 'rm', '-rf', os.path.join(backup_dir_path, old_backup)], check=True) + print(f"Deleted oldest backup to maintain limit: {old_backup}") + + # Generate the backup folder name from the current git commit and branch name + branch = get_short_branch() + commit = get_commit_date()[12:-16] + backup_folder_name = f"{branch}_{commit}_auto" + + # Check if the backup folder already exists + backup_path = os.path.join(backup_dir_path, backup_folder_name) + + if os.path.exists(backup_path): + print(f"Backup folder {backup_folder_name} already exists. Skipping backup.") + return + + # Create the backup directory and copy openpilot to it + Path(backup_path).mkdir(parents=True, exist_ok=True) + subprocess.run(['sudo', 'cp', '-a', '/data/openpilot/.', backup_path + '/'], check=True) + print(f"Successfully backed up openpilot to {backup_folder_name}.") + def main() -> None: # Create the long term param storage folder try: @@ -413,6 +444,13 @@ def main() -> None: except subprocess.CalledProcessError as e: print(f"Failed to remount /persist. Error: {e}") + # Backup the current version of openpilot + try: + backup_thread = threading.Thread(target=backup_openpilot) + backup_thread.start() + except subprocess.CalledProcessError as e: + print(f"Failed to backup openpilot. Error: {e}") + manager_init() if os.getenv("PREPAREONLY") is not None: return diff --git a/selfdrive/ui/qt/offroad/settings.cc b/selfdrive/ui/qt/offroad/settings.cc index c7bac9b..58c1904 100644 --- a/selfdrive/ui/qt/offroad/settings.cc +++ b/selfdrive/ui/qt/offroad/settings.cc @@ -2,6 +2,8 @@ #include #include +#include +#include #include #include #include @@ -268,6 +270,187 @@ DevicePanel::DevicePanel(SettingsWindow *parent) : ListWidget(parent) { }); addItem(deleteStorageParamsBtn); + // Backup FrogPilot + std::vector frogpilotBackupOptions{tr("Backup"), tr("Delete"), tr("Restore")}; + FrogPilotButtonsControl *frogpilotBackup = new FrogPilotButtonsControl("FrogPilot Backups", "Backup, delete, or restore your FrogPilot backups.", "", frogpilotBackupOptions); + + connect(frogpilotBackup, &FrogPilotButtonsControl::buttonClicked, [=](int id) { + QDir backupDir("/data/backups"); + + if (id == 0) { + QString nameSelection = InputDialog::getText(tr("Name your backup"), this, "", false, 1); + if (!nameSelection.isEmpty()) { + std::thread([=]() { + frogpilotBackup->setValue("Backing up..."); + + std::string fullBackupPath = backupDir.absolutePath().toStdString() + "/" + nameSelection.toStdString(); + + std::ostringstream commandStream; + commandStream << "mkdir -p " << std::quoted(fullBackupPath) + << " && rsync -av /data/openpilot/ " << std::quoted(fullBackupPath + "/"); + std::string command = commandStream.str(); + + int result = std::system(command.c_str()); + if (result == 0) { + std::cout << "Backup successful to " << fullBackupPath << std::endl; + frogpilotBackup->setValue("Success!"); + std::this_thread::sleep_for(std::chrono::seconds(3)); + frogpilotBackup->setValue(""); + } else { + std::cerr << "Backup failed with error code: " << result << std::endl; + frogpilotBackup->setValue("Failed..."); + std::this_thread::sleep_for(std::chrono::seconds(3)); + frogpilotBackup->setValue(""); + } + }).detach(); + } + } else if (id == 1) { + QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QString selection = MultiOptionDialog::getSelection(tr("Select a backup to delete"), backupNames, "", this); + if (!selection.isEmpty()) { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to delete this backup?"), tr("Delete"), this)) return; + std::thread([=]() { + frogpilotBackup->setValue("Deleting..."); + QDir dirToDelete(backupDir.absoluteFilePath(selection)); + if (dirToDelete.removeRecursively()) { + frogpilotBackup->setValue("Deleted!"); + } else { + frogpilotBackup->setValue("Failed..."); + } + std::this_thread::sleep_for(std::chrono::seconds(3)); + frogpilotBackup->setValue(""); + }).detach(); + } + } else { + QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QString selection = MultiOptionDialog::getSelection(tr("Select a restore point"), backupNames, "", this); + if (!selection.isEmpty()) { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to restore this version of FrogPilot?"), tr("Restore"), this)) return; + std::thread([=]() { + frogpilotBackup->setValue("Restoring..."); + + std::string sourcePath = backupDir.absolutePath().toStdString() + "/" + selection.toStdString(); + std::string targetPath = "/data/safe_staging/finalized"; + std::string consistentFilePath = targetPath + "/.overlay_consistent"; + + std::ostringstream commandStream; + commandStream << "rsync -av --delete --exclude='.overlay_consistent' " << std::quoted(sourcePath + "/") << " " << std::quoted(targetPath + "/"); + std::string command = commandStream.str(); + int result = std::system(command.c_str()); + + if (result == 0) { + std::cout << "Restore successful from " << sourcePath << " to " << targetPath << std::endl; + std::ofstream consistentFile(consistentFilePath); + if (consistentFile) { + consistentFile.close(); + std::cout << ".overlay_consistent file created successfully." << std::endl; + } else { + std::cerr << "Failed to create .overlay_consistent file." << std::endl; + frogpilotBackup->setValue("Failed..."); + std::this_thread::sleep_for(std::chrono::seconds(3)); + frogpilotBackup->setValue(""); + } + Hardware::reboot(); + } else { + std::cerr << "Restore failed with error code: " << result << std::endl; + } + }).detach(); + } + } + }); + addItem(frogpilotBackup); + + // Backup toggles + std::vector toggleBackupOptions{tr("Backup"), tr("Delete"), tr("Restore")}; + FrogPilotButtonsControl *toggleBackup = new FrogPilotButtonsControl("Toggle Backups", "Backup, delete, or restore your toggle backups.", "", toggleBackupOptions); + + connect(toggleBackup, &FrogPilotButtonsControl::buttonClicked, [=](int id) { + QDir backupDir("/data/toggle_backups"); + + if (id == 0) { + QString nameSelection = InputDialog::getText(tr("Name your backup"), this, "", false, 1); + if (!nameSelection.isEmpty()) { + std::thread([=]() { + toggleBackup->setValue("Backing up..."); + + std::string fullBackupPath = backupDir.absolutePath().toStdString() + "/" + nameSelection.toStdString() + "/"; + + std::ostringstream commandStream; + commandStream << "mkdir -p " << std::quoted(fullBackupPath) + << " && rsync -av /data/params/d/ " << std::quoted(fullBackupPath); + std::string command = commandStream.str(); + + int result = std::system(command.c_str()); + if (result == 0) { + std::cout << "Backup successful to " << fullBackupPath << std::endl; + toggleBackup->setValue("Success!"); + std::this_thread::sleep_for(std::chrono::seconds(3)); + toggleBackup->setValue(""); + } else { + std::cerr << "Backup failed with error code: " << result << std::endl; + toggleBackup->setValue("Failed..."); + std::this_thread::sleep_for(std::chrono::seconds(3)); + toggleBackup->setValue(""); + } + }).detach(); + } + } else if (id == 1) { + QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QString selection = MultiOptionDialog::getSelection(tr("Select a backup to delete"), backupNames, "", this); + if (!selection.isEmpty()) { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to delete this backup?"), tr("Delete"), this)) return; + std::thread([=]() { + toggleBackup->setValue("Deleting..."); + QDir dirToDelete(backupDir.absoluteFilePath(selection)); + if (dirToDelete.removeRecursively()) { + toggleBackup->setValue("Deleted!"); + } else { + toggleBackup->setValue("Failed..."); + } + std::this_thread::sleep_for(std::chrono::seconds(3)); + toggleBackup->setValue(""); + }).detach(); + } + } else { + QStringList backupNames = backupDir.entryList(QDir::Dirs | QDir::NoDotAndDotDot); + + QString selection = MultiOptionDialog::getSelection(tr("Select a restore point"), backupNames, "", this); + if (!selection.isEmpty()) { + if (!ConfirmationDialog::confirm(tr("Are you sure you want to restore this toggle backup?"), tr("Restore"), this)) return; + std::thread([=]() { + toggleBackup->setValue("Restoring..."); + + std::string sourcePath = backupDir.absolutePath().toStdString() + "/" + selection.toStdString() + "/"; + std::string targetPath = "/data/params/d/"; + + std::ostringstream commandStream; + commandStream << "rsync -av --delete " << std::quoted(sourcePath) << " " << std::quoted(targetPath); + std::string command = commandStream.str(); + + int result = std::system(command.c_str()); + + if (result == 0) { + std::cout << "Restore successful from " << sourcePath << " to " << targetPath << std::endl; + toggleBackup->setValue("Success!"); + paramsMemory.putBool("FrogPilotTogglesUpdated", true); + std::this_thread::sleep_for(std::chrono::seconds(3)); + toggleBackup->setValue(""); + paramsMemory.putBool("FrogPilotTogglesUpdated", false); + } else { + std::cerr << "Restore failed with error code: " << result << std::endl; + toggleBackup->setValue("Failed..."); + std::this_thread::sleep_for(std::chrono::seconds(3)); + toggleBackup->setValue(""); + } + }).detach(); + } + } + }); + addItem(toggleBackup); + QObject::connect(uiState(), &UIState::offroadTransition, [=](bool offroad) { for (auto btn : findChildren()) { btn->setEnabled(offroad);