add builtin updater

This commit is contained in:
Lightos1
2026-06-14 17:28:56 +02:00
parent 9725b1ba8f
commit e429e6ad44
6 changed files with 379 additions and 18 deletions

View File

@@ -69,7 +69,7 @@ CXXFLAGS := $(CFLAGS) -fno-exceptions -std=gnu++23
ASFLAGS := -g $(ARCH) ASFLAGS := -g $(ARCH)
LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map) LDFLAGS = -specs=$(DEVKITPRO)/libnx/switch.specs -g $(ARCH) -Wl,-Map,$(notdir $*.map)
LIBS := -lcurl -lz -lzzip -lmbedtls -lmbedx509 -lmbedcrypto -ljansson -lnx LIBS := -lcurl -lz -lminizip -lmbedtls -lmbedx509 -lmbedcrypto -ljansson -lnx
#--------------------------------------------------------------------------------- #---------------------------------------------------------------------------------
# list of directories containing libraries, this must be the top level containing # list of directories containing libraries, this must be the top level containing

View File

@@ -31,7 +31,7 @@
#include "main_gui.h" #include "main_gui.h"
#include "misc_gui.h" #include "misc_gui.h"
#include "ult_ext.h" #include "ult_ext.h"
#include "update_gui.h"
tsl::elm::Element *MainGui::baseUI() { tsl::elm::Element *MainGui::baseUI() {
auto *list = new BoxClippedList(); auto *list = new BoxClippedList();
@@ -110,6 +110,18 @@ void MainGui::listUI() {
return false; return false;
}); });
this->listElement->addItem(aboutItem); this->listElement->addItem(aboutItem);
tsl::elm::ListItem *updateItem = new tsl::elm::ListItem("Updates");
updateItem->setClickListener([this](u64 keys) {
if ((keys & HidNpadButton_A) == HidNpadButton_A && this->context) {
tsl::changeTo<UpdateGui>();
return true;
}
return false;
});
this->listElement->addItem(updateItem);
} }
void MainGui::refresh() { void MainGui::refresh() {

View File

@@ -0,0 +1,269 @@
/*
* Copyright (c) Souldbminer, Lightos_ and Horizon OC Contributors
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#include "update_gui.h"
#include "ult_ext.h"
#include "base_menu_gui.h"
#include "../format.h"
#include "../style.h"
#include <tesla.hpp>
#include <ultra.hpp>
#include <cstdio>
#include <string>
static constexpr u16 PANEL_H = 80;
class UpdateStatusPanel : public tsl::elm::Element {
public:
void setState(int stage, int progress,
const std::string &pkg, const std::string &msg) {
m_stage = stage;
m_progress = progress;
m_pkg = pkg;
m_msg = msg;
}
void draw(tsl::gfx::Renderer *renderer) override {
if (this->getHeight() == 0) return;
const s32 PAD = 20;
const s32 x = this->getX() + PAD;
const s32 y = this->getY();
const s32 w = this->getWidth() - PAD * 2;
if (m_stage == 0) {
return;
}
if (m_stage == 1 || m_stage == 2) {
const char *opLabel = (m_stage == 1) ? "Downloading" : "Extracting";
tsl::Color accent = (m_stage == 1)
? tsl::Color{0x4, 0x8, 0xF, 0xF} // blue
: tsl::Color{0x3, 0xA, 0x8, 0xF}; // teal
renderer->drawString(opLabel, false, x, y + 20, 15, accent);
renderer->drawString(m_pkg.c_str(), false, x + 104, y + 20, 15,
tsl::sectionTextColor);
const s32 barY = y + 48;
renderer->drawRect(x, barY, w, 7, tsl::Color{0x3, 0x3, 0x3, 0xF});
int pct = (m_progress < 0) ? 0 : (m_progress > 100 ? 100 : m_progress);
s32 fillW = (s32)((long)w * pct / 100);
if (fillW > 0)
renderer->drawRect(x, barY, fillW, 7, accent);
char buf[8];
if (m_progress < 0)
std::snprintf(buf, sizeof(buf), "...");
else
std::snprintf(buf, sizeof(buf), "%d%%", pct);
u32 tw = renderer->getTextDimensions(buf, false, 13).first;
renderer->drawString(buf, false,
x + w - (s32)tw, barY - 14, 13, tsl::infoTextColor);
} else {
tsl::Color col;
switch (m_stage) {
case 3: col = {0x3, 0xB, 0x5, 0xF}; break; // Done → green
case 4: col = {0xE, 0x3, 0x3, 0xF}; break; // Failed → red
default: col = {0x7, 0x7, 0x7, 0xF}; break; // Cancel → grey
}
renderer->drawString(m_msg.c_str(), false, x, y + 30, 14, col);
}
}
void layout(u16, u16, u16, u16) override {
this->setBoundaries(this->getX(), this->getY(),
this->getWidth(), PANEL_H);
}
Element *requestFocus(Element *, tsl::FocusDirection) override {
return nullptr;
}
private:
int m_stage = 0;
int m_progress = 0;
std::string m_pkg;
std::string m_msg;
};
UpdateGui::UpdateGui() = default;
UpdateGui::~UpdateGui() { reapThread(); }
void UpdateGui::startJob(int packageIndex, bool extractOnly) {
if (isBusy()) return;
m_activePackage = packageIndex;
m_extractOnly = extractOnly;
m_resultMessage.clear();
ult::abortDownload.store(false, std::memory_order_release);
ult::abortUnzip.store(false, std::memory_order_release);
m_stage.store(UpdateStage::Downloading, std::memory_order_release);
if (R_SUCCEEDED(threadCreate(&m_thread, jobEntry, this,
nullptr, 0x8000, 0x20, -2))) {
m_threadActive = true;
threadStart(&m_thread);
} else {
m_resultMessage = "Failed to create thread";
m_stage.store(UpdateStage::Failed, std::memory_order_release);
m_threadActive = false;
}
}
void UpdateGui::requestCancel() {
ult::abortDownload.store(true, std::memory_order_release);
ult::abortUnzip.store(true, std::memory_order_release);
}
void UpdateGui::reapThread() {
if (m_threadActive) {
threadWaitForExit(&m_thread);
threadClose(&m_thread);
m_threadActive = false;
}
}
/* static */ void UpdateGui::jobEntry(void *arg) {
static_cast<UpdateGui *>(arg)->jobBody();
}
void UpdateGui::jobBody() {
const PackageInfo &pkg = kPackages[m_activePackage];
if (!m_extractOnly) {
m_stage.store(UpdateStage::Downloading, std::memory_order_release);
ult::createDirectory("sdmc:/config/horizon-oc/");
bool ok = ult::downloadFile(pkg.url, pkg.zipPath, false, false);
if (!ok || ult::abortDownload.load(std::memory_order_acquire)) {
m_resultMessage = ult::abortDownload.load(std::memory_order_acquire)
? "Download cancelled"
: "Download failed \u2014 check network";
m_stage.store(
ult::abortDownload.load() ? UpdateStage::Cancelled : UpdateStage::Failed,
std::memory_order_release);
return;
}
}
m_stage.store(UpdateStage::Extracting, std::memory_order_release);
bool ok = ult::unzipFile(pkg.zipPath, "sdmc:/");
if (!ok || ult::abortUnzip.load(std::memory_order_acquire)) {
m_resultMessage = ult::abortUnzip.load(std::memory_order_acquire)
? "Extraction cancelled"
: "Extraction failed \u2014 archive may be corrupt";
m_stage.store(
ult::abortUnzip.load() ? UpdateStage::Cancelled : UpdateStage::Failed,
std::memory_order_release);
return;
}
m_resultMessage = "Update complete! Restart required.";
m_stage.store(UpdateStage::Done, std::memory_order_release);
}
void UpdateGui::listUI() {
BaseMenuGui::refresh();
if (!this->context) return;
this->listElement->addItem(new tsl::elm::CategoryHeader("Packages"));
for (int i = 0; i < 2; ++i) {
auto *item = new tsl::elm::ListItem(kPackages[i].displayName);
item->setValue("\uE0E0");
const int idx = i;
item->setClickListener([this, idx](u64 keys) -> bool {
if (isBusy()) return false;
if (keys & HidNpadButton_A) {
startJob(idx, false);
return true;
}
return false;
});
m_items[i] = item;
this->listElement->addItem(item);
}
this->listElement->addItem(new tsl::elm::CategoryHeader("Status"));
auto *panel = new UpdateStatusPanel();
panel->setState(0, 0, "", "");
m_status = panel;
this->listElement->addItem(panel);
}
void UpdateGui::pollJob() {
auto stage = m_stage.load(std::memory_order_acquire);
if (stage == UpdateStage::Idle || !m_status) return;
const char *pkgName = (m_activePackage >= 0 && m_activePackage < 2)
? kPackages[m_activePackage].displayName : "";
if (stage == UpdateStage::Downloading) {
int pct = ult::downloadPercentage.load(std::memory_order_relaxed);
m_status->setState(1, pct, pkgName, "");
if (m_items[m_activePackage])
m_items[m_activePackage]->setValue("...");
return;
}
if (stage == UpdateStage::Extracting) {
int pct = ult::unzipPercentage.load(std::memory_order_relaxed);
m_status->setState(2, pct, pkgName, "");
if (m_items[m_activePackage])
m_items[m_activePackage]->setValue("...");
return;
}
int panelStage = (stage == UpdateStage::Done) ? 3
: (stage == UpdateStage::Failed) ? 4 : 5;
m_status->setState(panelStage, 100, pkgName, m_resultMessage);
for (int i = 0; i < 2; ++i)
if (m_items[i]) m_items[i]->setValue("\uE0E0");
reapThread();
m_stage.store(UpdateStage::Idle, std::memory_order_release);
}
void UpdateGui::update() {
BaseMenuGui::update();
pollJob();
}
bool UpdateGui::handleInput(u64 keysDown, u64 keysHeld,
const HidTouchState &touchPos,
HidAnalogStickState leftJoy,
HidAnalogStickState rightJoy) {
if ((keysDown & HidNpadButton_R) && isBusy()) {
requestCancel();
return true;
}
return false;
}

View File

@@ -0,0 +1,80 @@
/*
* Copyright (c) Souldbminer, Lightos_ and Horizon OC Contributors
*
* This program is free software; you can redistribute it and/or modify it
* under the terms and conditions of the GNU General Public License,
* version 2, as published by the Free Software Foundation.
*
* This program is distributed in the hope it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
#pragma once
#include "../../ipc.h"
#include <atomic>
#include <string>
#include "base_menu_gui.h"
class UpdateStatusPanel;
class UpdateGui : public BaseMenuGui {
public:
UpdateGui();
~UpdateGui();
void listUI() override;
void update() override;
bool handleInput(u64 keysDown, u64 keysHeld, const HidTouchState &touchPos,
HidAnalogStickState leftJoy, HidAnalogStickState rightJoy) override;
private:
enum class UpdateStage {
Idle,
Downloading,
Extracting,
Done,
Failed,
Cancelled,
};
struct PackageInfo {
const char *displayName;
const char *url;
const char *zipPath;
};
static constexpr PackageInfo kPackages[2] = {
{"HOC", "https://github.com/Horizon-OC/Horizon-OC/releases/latest/download/dist.zip", "sdmc:/config/horizon-oc/dist.zip"},
{"HOC-EXT", "https://github.com/Horizon-OC/Horizon-OC/releases/latest/download/dist_ext.zip", "sdmc:/config/horizon-oc/dist_ext.zip"},
};
bool isBusy() const {
return m_stage.load(std::memory_order_acquire) != UpdateStage::Idle;
}
void startJob(int packageIndex, bool extractOnly);
void requestCancel();
void reapThread();
void pollJob();
static void jobEntry(void *arg);
void jobBody();
std::atomic<UpdateStage> m_stage{UpdateStage::Idle};
int m_activePackage = -1;
bool m_extractOnly = false;
bool m_threadActive = false;
Thread m_thread{};
std::string m_resultMessage;
tsl::elm::ListItem *m_items[2] = {nullptr, nullptr};
UpdateStatusPanel *m_status = nullptr;
};