Updates: Frame Studio and Frame Home update themselves from GitHub

CI stamps every Studio build with its commit (FRAME_BUILD) and the
release gets latest.json: the commit, Frame Home's identity (a hash of
the headset programs in frame/payload) and the files in the release.

Frame Studio (studio/updater.h): checks a few seconds after start and
every 6 hours (Help > Check for Updates any time). A newer build shows
"Update Available" in the top bar; Download and Install fetches this
computer's build with the system's curl, unpacks it with tar, and swaps
each item in (the old one renamed aside first, so the running copy
keeps working and a failure rolls everything back). Restart Now closes
through the normal unsaved-changes prompt, then starts the new copy;
the next start deletes what was set aside. Local builds never update.
New platform calls: systemTool (System32's curl/tar on Windows) and
launch.

Frame Home (src/switcher.cpp): on the Steam Frame the Environments
panel checks a minute after it starts and every 6 hours. When the
installed programs' hash isn't the release's, "Update Frame Home"
shows beside Restart Frame Home; it downloads FrameHome-installer.zip
and runs its install.sh in its own session (which restarts Frame Home
and the panel), and the new panel says "Frame Home is updated". Also
frame-switcher --check-update / --update from a terminal.
frame/payload: frame-switcher rebuilt on the Steam Frame with this.

Tested: Studio's full cycle (check, download, unpack, swap, relaunch,
cleanup) and its rollback against a local fake release on Windows; the
headset's check and update against a fake release on the Frame (old
panel and Frame Home replaced, then up to date); Linux compile of
Studio; Studio --selftest unchanged (98 passed, same 6 known fails).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ToastconcernandClaude Opus 5.5 committed 2026-10-23 11:40:23 -05:00
1 parent 39c8424391
commit fedadd5ae3
12 files changed
+590 -3

No files matched your search

+65
View File
@@ -27,6 +27,7 @@
#include "world_runtime.h"
#include "frame_pacer.h"
#include "platform.h"
#include "updater.h"
#define STB_IMAGE_WRITE_IMPLEMENTATION
#define STBI_WRITE_NO_STDIO
#include "stb_image_write.h"
@@ -665,6 +666,10 @@ struct Studio {
int undoGroup = 0, nextUndoGroup = 1; // pushUndo tags steps with undoGroup while a batch is open
std::vector<std::vector<std::string>> recent; // File > Open Recent: each entry footprint [+ vista]
bool showControls = false; // Help > Controls
updater::Updater updates; // Frame Studio's own updates (studio/updater.h)
bool showUpdate = false; // the update window (Help > Check for Updates, or the top bar's button)
bool restartAfterClose = false; // "Restart": start the updated copy once this one has closed
std::chrono::steady_clock::time_point nextUpdateCheck = std::chrono::steady_clock::now() + std::chrono::seconds(5);
bool testing = false; // --selftest: leave the recent list and edits alone
struct TestEye { ImVec2 at; std::vector<size_t> members; };
std::vector<TestEye> testEyes; // --selftest: the explorer's eye buttons this frame (folders and rows)
@@ -6403,6 +6408,8 @@ struct Studio {
if (ImGui::BeginMenu("Help")) {
if (ImGui::MenuItem("Controls", "F1", showControls)) showControls = !showControls;
if (ImGui::MenuItem("Menu Search", "F3")) openSearch();
ImGui::Separator();
if (ImGui::MenuItem("Check for Updates…")) { showUpdate = true; updates.check(); }
ImGui::EndMenu();
}
// The workspace tabs: the active one raised like Blender's
@@ -6423,6 +6430,17 @@ struct Studio {
dl->AddText(ImVec2(a.x + (bw - ts.x) * 0.5f, a.y + (bh - ts.y) * 0.5f), col(on ? 0xffffff : kDim), kWorkspaceName[w]);
ImGui::SameLine(0, 2 * scale);
}
const updater::State us = updates.state();
if (us == updater::State::Available || us == updater::State::Installing || us == updater::State::Ready) {
ImGui::Dummy(ImVec2(10 * scale, 0));
ImGui::SameLine(0, 0);
ImGui::PushStyleColor(ImGuiCol_Button, hex(kOrange));
ImGui::PushStyleColor(ImGuiCol_ButtonHovered, hex(kOrange));
ImGui::PushStyleColor(ImGuiCol_Text, hex(0xffffff));
const char* label = us == updater::State::Ready ? "Restart to Update###upd" : us == updater::State::Installing ? "Updating…###upd" : "Update Available###upd";
if (ImGui::SmallButton(label)) showUpdate = true;
ImGui::PopStyleColor(3);
}
if (scene) { // the open home's name, at the right end as Blender shows its scene
const std::string name = shownName() + (dirty ? " *" : "");
const float w = ImGui::CalcTextSize(name.c_str()).x;
@@ -7354,6 +7372,7 @@ struct Studio {
addPopup();
searchPopup();
controlsWindow();
updateWindow();
settingsWindow();
reportWindow();
headsetDebugWindow();
@@ -7974,6 +7993,49 @@ struct Studio {
}
ImGui::End();
}
// Updates: checked a few seconds after start and every 6 hours (Help > Check for Updates any time); the top bar
// shows a button when there is one. Installing happens only when asked, here.
void updateWindow() {
const auto now = std::chrono::steady_clock::now();
if (!testing && !g_selfTestRun && updater::Updater::canUpdate() && now >= nextUpdateCheck) {
nextUpdateCheck = now + std::chrono::hours(6);
updates.check();
}
if (!showUpdate) return;
ImGui::SetNextWindowSize(ImVec2(380 * scale, 0), ImGuiCond_Appearing);
ImGui::SetNextWindowPos(ImVec2(ImGui::GetIO().DisplaySize.x * 0.5f, ImGui::GetIO().DisplaySize.y * 0.4f), ImGuiCond_Appearing, ImVec2(0.5f, 0.5f));
if (!ImGui::Begin("Frame Studio Updates", &showUpdate, ImGuiWindowFlags_NoCollapse | ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoDocking)) { ImGui::End(); return; }
ImGui::PushTextWrapPos(360 * scale);
if (!updater::Updater::canUpdate()) {
ImGui::TextUnformatted("This copy of Frame Studio was built on this computer, not downloaded from GitHub, so it doesn't update itself.");
if (ImGui::Button("Open the Downloads Page")) platform::openPath(updater::kPageUrl);
ImGui::PopTextWrapPos();
ImGui::End();
return;
}
const updater::State s = updates.state();
ImGui::TextUnformatted(updates.text().c_str());
ImGui::TextDisabled("This build: %.7s", updater::kBuild);
ImGui::Spacing();
if (s == updater::State::Available) {
if (ImGui::Button("Download and Install")) updates.install();
ImGui::SameLine();
if (ImGui::Button("Later")) showUpdate = false;
} else if (s == updater::State::Ready) {
if (ImGui::Button("Restart Now")) { restartAfterClose = true; if (window.requestClose) window.requestClose(); }
ImGui::SameLine();
if (ImGui::Button("Later")) showUpdate = false;
} else if (s == updater::State::Failed) {
if (ImGui::Button("Try Again")) updates.check();
ImGui::SameLine();
if (ImGui::Button("Open the Downloads Page")) platform::openPath(updater::kPageUrl);
} else if (s == updater::State::Current) {
if (ImGui::Button("Check Again")) updates.check();
}
ImGui::PopTextWrapPos();
ImGui::End();
}
};
@@ -9416,6 +9478,7 @@ int runStudio(StudioWindow& window, const std::vector<std::string>& args) {
const float scale = window.uiScale();
platform::setDialogOwner(window.nativeHandle());
g_selfTestRun = !args.empty() && args[0] == "--selftest"; // (before anything saves)
updater::cleanUp(); // what the last update set aside
auto app = std::make_unique<Studio>();
g_app = app.get();
app->window = window.hooks();
@@ -9598,6 +9661,8 @@ int runStudio(StudioWindow& window, const std::vector<std::string>& args) {
vkDeviceWaitIdle(r.device());
window.stopHangWatchdog();
if (app->saver.joinable()) app->saver.join();
if (app->restartAfterClose && !platform::launch(updater::updatedProgram())) // Help > updates: Restart Now
platform::message("Frame Studio", "The updated Frame Studio didn't start. Open it again from its folder.", platform::Icon::Warning);
if (app->loadState == 1 || app->loadAbandoned) { // a home still being read: nothing to lose, and waiting for it is what stalls
std::fflush(stderr);
std::_Exit(0);