mirror of
https://github.com/mitch030504/Wiicompiled_VR_Frame.git
synced 2026-10-06 02:00:14 +02:00
Add --update and --check to the install script, and an Updates tab in the game
--update reuses the options an install was made with (saved to install.conf in the work dir) and builds nothing when the Frame already has the newest release and Retro Rewind pack; --check only reports. The installed release is stamped as .release-tag beside the game on the Frame. An install built on the Frame itself (--frame local) also gets a systemd user service the game's new Updates tab starts: the update runs in the background at low priority, writes each step and the build percentage to a status file the tab shows, and reopens the game at the end if it was closed. Progress does not go through Steam notifications: on the Frame, Steam receives them from steam_notif_daemon but does not display them. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01UyuWx5hNrMpf9R78FZFcbA
This commit is contained in:
6 files changed
+851
-30
No files matched your search
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <string>
|
||||
|
||||
// Settings > Updates: whether a newer release of the game exists, and, on a Frame that holds its own
|
||||
// build, updating to it without leaving the headset.
|
||||
//
|
||||
// Both answers come from Launcher/steam-frame-install.sh, which already knows how to compare
|
||||
// versions and how to build: `--check` reports, `--update` installs. The update reports its progress
|
||||
// through a status file rather than Steam notifications, which the Frame's Steam does not show.
|
||||
// The install writes update.conf beside the player's configuration when it put a build on this
|
||||
// machine, so the Update button appears only where it can actually work; installed from a PC, the
|
||||
// tab says to update there.
|
||||
namespace update_check {
|
||||
|
||||
enum class State {
|
||||
// This install cannot update itself: it was built on a PC, which is where updates are run.
|
||||
PcInstall,
|
||||
// It can, and nothing has been asked of it yet this session.
|
||||
Idle,
|
||||
Checking,
|
||||
UpToDate,
|
||||
Available,
|
||||
// An update is running beside the game, as a service of its own: it carries on if the game is
|
||||
// closed, and then opens it again when it is done.
|
||||
Updating,
|
||||
Failed,
|
||||
};
|
||||
|
||||
struct Status {
|
||||
State state = State::PcInstall;
|
||||
// The release this build came from, or empty when it was built from a source tree.
|
||||
std::string installedRelease;
|
||||
// One line under the state: which release is available, what failed, or what step is running.
|
||||
std::string detail;
|
||||
// An update has installed a different build since this one started: it takes effect when the
|
||||
// game is started again.
|
||||
bool restartNeeded = false;
|
||||
};
|
||||
|
||||
// What the Updates tab draws. Cheap enough to call every frame: a check or update in flight is
|
||||
// answered from memory, and otherwise the files behind it are re-read at most once a second.
|
||||
Status Current();
|
||||
|
||||
// Whether there is an Updates tab to show at all: this install can update itself, or it at least
|
||||
// knows which release it is running. Neither is true of a build that came from somewhere else.
|
||||
inline bool Relevant(const Status& status) {
|
||||
return status.state != State::PcInstall || !status.installedRelease.empty();
|
||||
}
|
||||
|
||||
// Asks the install script whether a newer release exists, in the background. Returns at once.
|
||||
void StartCheck();
|
||||
|
||||
// Starts the update in the background: its own systemd user service runs the install script, which
|
||||
// reports each step to the file Current() reads, so the game can stay open and show the progress.
|
||||
// On false, `error` says why, for display.
|
||||
bool StartUpdate(std::string& error);
|
||||
|
||||
} // namespace update_check
|
||||
@@ -11,6 +11,7 @@
|
||||
#include "music_attenuation.h"
|
||||
#include "runtime_config.h"
|
||||
#include "runtime_log.h"
|
||||
#include "update_check.h"
|
||||
#include "vr/camera_toggle.h"
|
||||
#include "vr/mkw_vr_culling.h"
|
||||
#include "vr/mkw_vr_first_person.h"
|
||||
@@ -2094,6 +2095,107 @@ void DrawDiagnosticsSettings() {
|
||||
}
|
||||
}
|
||||
|
||||
bool g_updatePromptOpen = false;
|
||||
std::string g_updateError;
|
||||
|
||||
void DrawUpdatePrompt() {
|
||||
constexpr const char* kTitle = "Update";
|
||||
if (g_updatePromptOpen && !ImGui::IsPopupOpen(kTitle)) ImGui::OpenPopup(kTitle);
|
||||
if (!ImGui::BeginPopupModal(kTitle, &g_updatePromptOpen, ImGuiWindowFlags_AlwaysAutoResize)) return;
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||
ImGui::TextUnformatted("The update is built here on the Frame, in the background, which takes a "
|
||||
"while: keep the Frame on its charger. You can keep playing meanwhile, "
|
||||
"though the game may stutter while it compiles. Close the game whenever "
|
||||
"you like: the update carries on and opens it again when it is done.");
|
||||
ImGui::PopTextWrapPos();
|
||||
if (ImGui::Button("Start the update", ImVec2(Scaled(180.0f), 0.0f))) {
|
||||
update_check::StartUpdate(g_updateError);
|
||||
g_updatePromptOpen = false;
|
||||
}
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Cancel", ImVec2(Scaled(120.0f), 0.0f))) g_updatePromptOpen = false;
|
||||
if (!g_updatePromptOpen) ImGui::CloseCurrentPopup();
|
||||
ImGui::EndPopup();
|
||||
}
|
||||
|
||||
void DrawUpdateSettings() {
|
||||
const update_check::Status status = update_check::Current();
|
||||
ImGui::PushTextWrapPos(ImGui::GetCursorPosX() + Scaled(380.0f));
|
||||
if (status.installedRelease.empty()) {
|
||||
ImGui::TextUnformatted("This build came from a source tree rather than a release.");
|
||||
} else {
|
||||
ImGui::Text("Release %s", status.installedRelease.c_str());
|
||||
}
|
||||
|
||||
const bool busy = status.state == update_check::State::Checking ||
|
||||
status.state == update_check::State::Updating;
|
||||
switch (status.state) {
|
||||
case update_check::State::PcInstall:
|
||||
ImGui::TextDisabled("The PC that built this installs its updates. Run the installer there "
|
||||
"again with --update.");
|
||||
break;
|
||||
case update_check::State::Idle:
|
||||
break;
|
||||
case update_check::State::Checking:
|
||||
ImGui::TextDisabled("Looking for a newer release...");
|
||||
break;
|
||||
case update_check::State::UpToDate:
|
||||
ImGui::TextDisabled("%s", status.detail.c_str());
|
||||
break;
|
||||
case update_check::State::Available:
|
||||
ImGui::TextUnformatted(status.detail.c_str());
|
||||
break;
|
||||
case update_check::State::Updating:
|
||||
ImGui::Text("Updating: %s", status.detail.c_str());
|
||||
ImGui::TextDisabled("Keep playing, or close the game: the update carries on and opens it "
|
||||
"again when it is done.");
|
||||
break;
|
||||
case update_check::State::Failed:
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", status.detail.c_str());
|
||||
break;
|
||||
}
|
||||
if (!g_updateError.empty()) {
|
||||
ImGui::TextColored(ImVec4(1.0f, 0.45f, 0.35f, 1.0f), "%s", g_updateError.c_str());
|
||||
}
|
||||
if (status.restartNeeded && status.state != update_check::State::Updating) {
|
||||
// The update replaced the build on disk; this process is still the old one.
|
||||
ImGui::TextUnformatted("The update is installed. Quit and start the game again to use it.");
|
||||
}
|
||||
ImGui::PopTextWrapPos();
|
||||
|
||||
if (status.state == update_check::State::PcInstall) {
|
||||
return;
|
||||
}
|
||||
if (status.restartNeeded && status.state != update_check::State::Updating) {
|
||||
if (ImGui::Button("Quit")) g_exitPromptOpen = true;
|
||||
ImGui::SameLine();
|
||||
}
|
||||
ImGui::BeginDisabled(busy);
|
||||
if (ImGui::Button(status.state == update_check::State::Idle ? "Check for updates" : "Check again")) {
|
||||
g_updateError.clear();
|
||||
update_check::StartCheck();
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Asks github.com for the newest release, and Retro Rewind's server for\n"
|
||||
"its newest pack when this install has one. Nothing is downloaded\n"
|
||||
"until you choose to update.");
|
||||
}
|
||||
if (status.state == update_check::State::Available) {
|
||||
ImGui::SameLine();
|
||||
if (ImGui::Button("Update now")) {
|
||||
g_updateError.clear();
|
||||
g_updatePromptOpen = true;
|
||||
}
|
||||
if (ImGui::IsItemHovered()) {
|
||||
ImGui::SetTooltip("Builds the update here on the Frame, in the background. The game\n"
|
||||
"can stay open and shows the progress here.");
|
||||
}
|
||||
}
|
||||
ImGui::EndDisabled();
|
||||
// The prompt itself is drawn at the top level, as the exit prompt is: the tab lives both in the
|
||||
// headset's panel and in a desktop menu, and a modal must outlive whichever opened it.
|
||||
}
|
||||
|
||||
void DrawFpsOverlay() {
|
||||
AuroraPresentTiming presentTiming{};
|
||||
aurora_get_present_timing(&presentTiming);
|
||||
@@ -2291,6 +2393,11 @@ void DrawTopBar() {
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
if (update_check::Relevant(update_check::Current()) && ImGui::BeginMenu("Updates")) {
|
||||
DrawUpdateSettings();
|
||||
ImGui::EndMenu();
|
||||
}
|
||||
|
||||
const ImGuiStyle& style = ImGui::GetStyle();
|
||||
const float hideWidth = ImGui::CalcTextSize("Hide (F10)").x + style.FramePadding.x * 2.0f;
|
||||
const float exitWidth = ImGui::CalcTextSize("X").x + style.FramePadding.x * 2.0f;
|
||||
@@ -2479,6 +2586,9 @@ void DrawVrSettingsPanelWindow() {
|
||||
tab("Controllers", DrawControllerSettings);
|
||||
tab("Audio", DrawAudioSettings);
|
||||
tab("Diagnostics", DrawDiagnosticsSettings);
|
||||
if (update_check::Relevant(update_check::Current())) {
|
||||
tab("Updates", DrawUpdateSettings);
|
||||
}
|
||||
ImGui::EndTabBar();
|
||||
}
|
||||
}
|
||||
@@ -2687,6 +2797,7 @@ void Draw() noexcept {
|
||||
DrawFpsOverlay();
|
||||
DrawTopBar();
|
||||
DrawExitPrompt();
|
||||
DrawUpdatePrompt();
|
||||
controller_mapping_wizard::Draw();
|
||||
// The wizard captures raw presses; keep them out of the game.
|
||||
const bool inputBlocked = controller_mapping_wizard::IsActive() || g_rebind.active;
|
||||
|
||||
@@ -0,0 +1,368 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "update_check.h"
|
||||
|
||||
#include "runtime_config.h"
|
||||
|
||||
#include <array>
|
||||
#include <charconv>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <mutex>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <system_error>
|
||||
#include <thread>
|
||||
|
||||
// Updating itself is the Frame's build, installed by Launcher/steam-frame-install.sh: a desktop
|
||||
// Linux game that holds its own build directory. Everywhere else the tab only reports the release
|
||||
// it is running, which needs none of the machinery below.
|
||||
#if defined(__linux__) && !defined(__ANDROID__)
|
||||
#define MKW_UPDATE_CHECK_SUPPORTED 1
|
||||
#include <sys/wait.h>
|
||||
#else
|
||||
#define MKW_UPDATE_CHECK_SUPPORTED 0
|
||||
#endif
|
||||
|
||||
namespace update_check {
|
||||
namespace {
|
||||
|
||||
// Written by Launcher/steam-frame-install.sh when it built on this machine, naming itself and the
|
||||
// service that runs it. Its absence is what tells the tab this install is updated from a PC.
|
||||
constexpr std::string_view kConfigFileName = "update.conf";
|
||||
// An update's progress, and a check's answer. Separate files, so a check never overwrites the
|
||||
// result of the update that opened the game.
|
||||
constexpr std::string_view kStatusFileName = "update-status";
|
||||
constexpr std::string_view kCheckStatusFileName = "update-status-check";
|
||||
// What the Update button leaves for the update to pick up.
|
||||
constexpr std::string_view kRequestFileName = "update-request";
|
||||
// Stamped beside the executable by the install; absent for a build from a source tree.
|
||||
constexpr std::string_view kReleaseTagFileName = ".release-tag";
|
||||
|
||||
// A finished update's status belongs on screen when the game opens right afterwards, which is what
|
||||
// the update does. Older than this it is history, and the tab starts clean.
|
||||
constexpr auto kStatusLifetime = std::chrono::hours(1);
|
||||
// A running update rewrites its status at every step and every percent of its build, so one that
|
||||
// has said nothing for this long was stopped without a chance to say so (the Frame lost power).
|
||||
constexpr auto kRunningStatusLifetime = std::chrono::hours(2);
|
||||
// The tab is drawn every frame; the files behind it are not read anywhere near that often.
|
||||
constexpr auto kReadInterval = std::chrono::seconds(1);
|
||||
|
||||
std::string Trimmed(std::string_view text) {
|
||||
const auto first = text.find_first_not_of(" \t\r\n");
|
||||
if (first == std::string_view::npos) {
|
||||
return {};
|
||||
}
|
||||
return std::string(text.substr(first, text.find_last_not_of(" \t\r\n") - first + 1));
|
||||
}
|
||||
|
||||
std::string ReadFirstLine(const std::filesystem::path& file) {
|
||||
std::ifstream stream(file);
|
||||
std::string line;
|
||||
if (!stream || !std::getline(stream, line)) {
|
||||
return {};
|
||||
}
|
||||
return Trimmed(line);
|
||||
}
|
||||
|
||||
struct Config {
|
||||
std::filesystem::path script;
|
||||
// The install's work directory, which holds the build the update reuses. The script defaults to
|
||||
// the same place, but an install given --work-dir does not live there.
|
||||
std::filesystem::path workDir;
|
||||
std::string service;
|
||||
|
||||
bool Usable() const { return !script.empty() && !service.empty(); }
|
||||
};
|
||||
|
||||
Config ReadConfig() {
|
||||
Config config;
|
||||
#if !MKW_UPDATE_CHECK_SUPPORTED
|
||||
return config;
|
||||
#else
|
||||
std::ifstream stream(RuntimeConfigFile::ApplicationDataDirectory() / kConfigFileName);
|
||||
if (!stream) {
|
||||
return config;
|
||||
}
|
||||
std::string line;
|
||||
while (std::getline(stream, line)) {
|
||||
const auto separator = line.find('=');
|
||||
if (line.empty() || line.front() == '#' || separator == std::string::npos) {
|
||||
continue;
|
||||
}
|
||||
const std::string key = Trimmed(std::string_view(line).substr(0, separator));
|
||||
const std::string value = Trimmed(std::string_view(line).substr(separator + 1));
|
||||
if (key == "script") {
|
||||
config.script = value;
|
||||
} else if (key == "work_dir") {
|
||||
config.workDir = value;
|
||||
} else if (key == "service") {
|
||||
config.service = value;
|
||||
}
|
||||
}
|
||||
std::error_code ec;
|
||||
if (!config.script.empty() && !std::filesystem::is_regular_file(config.script, ec)) {
|
||||
// The work directory was removed, so there is nothing here to update from any more.
|
||||
config.script.clear();
|
||||
}
|
||||
return config;
|
||||
#endif
|
||||
}
|
||||
|
||||
std::string InstalledRelease() {
|
||||
if (const auto executableDirectory = RuntimeConfigFile::ExecutableDirectory()) {
|
||||
return ReadFirstLine(*executableDirectory / kReleaseTagFileName);
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
// What this session has been told, which outranks the files until the game is next started: a check
|
||||
// or update it asked for, and what came back.
|
||||
std::mutex g_mutex;
|
||||
bool g_sessionStateSet = false;
|
||||
State g_sessionState = State::Idle;
|
||||
std::string g_sessionDetail;
|
||||
// What was last read from disk, and when, so drawing the tab does not re-read it every frame.
|
||||
std::chrono::steady_clock::time_point g_lastRead;
|
||||
bool g_haveRead = false;
|
||||
bool g_haveFileState = false;
|
||||
State g_fileState = State::Idle;
|
||||
std::string g_fileDetail;
|
||||
bool g_canUpdate = false;
|
||||
// The release this process was started from, read once: an update replaces the stamp on disk while
|
||||
// the old build is still the one running. The stamp as it is now tells whether that has happened.
|
||||
std::string g_runningRelease;
|
||||
bool g_haveRunningRelease = false;
|
||||
std::string g_releaseOnDisk;
|
||||
|
||||
void SetSessionState(State state, std::string detail) {
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_sessionStateSet = true;
|
||||
g_sessionState = state;
|
||||
g_sessionDetail = std::move(detail);
|
||||
}
|
||||
|
||||
std::string ShellQuoted(const std::string& text) {
|
||||
std::string quoted = "'";
|
||||
for (const char character : text) {
|
||||
if (character == '\'') {
|
||||
quoted += "'\\''";
|
||||
} else {
|
||||
quoted += character;
|
||||
}
|
||||
}
|
||||
quoted += '\'';
|
||||
return quoted;
|
||||
}
|
||||
|
||||
// Runs `command`, returning its exit status and keeping the last line of its output for a message.
|
||||
int Run(const std::string& command, std::string& lastLine) {
|
||||
lastLine.clear();
|
||||
#if !MKW_UPDATE_CHECK_SUPPORTED
|
||||
(void)command;
|
||||
return -1;
|
||||
#else
|
||||
FILE* pipe = popen(command.c_str(), "r");
|
||||
if (pipe == nullptr) {
|
||||
return -1;
|
||||
}
|
||||
std::array<char, 512> buffer{};
|
||||
while (std::fgets(buffer.data(), static_cast<int>(buffer.size()), pipe) != nullptr) {
|
||||
if (const std::string line = Trimmed(buffer.data()); !line.empty()) {
|
||||
lastLine = line;
|
||||
}
|
||||
}
|
||||
const int status = pclose(pipe);
|
||||
if (status == -1 || !WIFEXITED(status)) {
|
||||
return -1;
|
||||
}
|
||||
return WEXITSTATUS(status);
|
||||
#endif
|
||||
}
|
||||
|
||||
// Reads a "<state>\t<unix time>\t<text>" line the script wrote. False when there is none, it is too
|
||||
// old to be about now, or it cannot be understood.
|
||||
bool ReadStatusFile(const std::filesystem::path& file, State& state, std::string& detail) {
|
||||
const std::string line = ReadFirstLine(file);
|
||||
if (line.empty()) {
|
||||
return false;
|
||||
}
|
||||
const auto firstTab = line.find('\t');
|
||||
const auto secondTab = line.find('\t', firstTab + 1);
|
||||
if (firstTab == std::string::npos || secondTab == std::string::npos) {
|
||||
return false;
|
||||
}
|
||||
const std::string written = line.substr(0, firstTab);
|
||||
long long seconds = 0;
|
||||
const char* const secondsBegin = line.data() + firstTab + 1;
|
||||
if (std::from_chars(secondsBegin, line.data() + secondTab, seconds).ec != std::errc{}) {
|
||||
return false;
|
||||
}
|
||||
const auto age = std::chrono::system_clock::now() -
|
||||
std::chrono::system_clock::time_point(std::chrono::seconds(seconds));
|
||||
const auto lifetime = written == "running" ? kRunningStatusLifetime : kStatusLifetime;
|
||||
if (age > lifetime || age < -lifetime) {
|
||||
return false;
|
||||
}
|
||||
if (written == "running") {
|
||||
state = State::Updating;
|
||||
} else if (written == "available") {
|
||||
state = State::Available;
|
||||
} else if (written == "uptodate" || written == "done") {
|
||||
state = State::UpToDate;
|
||||
} else if (written == "failed") {
|
||||
state = State::Failed;
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
detail = line.substr(secondTab + 1);
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
Status Current() {
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
// Drawn every frame, so the files behind it are read on a timer instead.
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (!g_haveRead || now - g_lastRead > kReadInterval) {
|
||||
g_releaseOnDisk = InstalledRelease();
|
||||
if (!g_haveRunningRelease) {
|
||||
g_runningRelease = g_releaseOnDisk;
|
||||
g_haveRunningRelease = true;
|
||||
}
|
||||
g_lastRead = now;
|
||||
g_haveRead = true;
|
||||
g_canUpdate = ReadConfig().Usable();
|
||||
g_fileState = State::Idle;
|
||||
g_fileDetail.clear();
|
||||
g_haveFileState = ReadStatusFile(RuntimeConfigFile::ApplicationDataDirectory() / kStatusFileName,
|
||||
g_fileState, g_fileDetail);
|
||||
}
|
||||
|
||||
Status status;
|
||||
status.installedRelease = g_runningRelease;
|
||||
status.restartNeeded = g_releaseOnDisk != g_runningRelease;
|
||||
if (!g_canUpdate) {
|
||||
status.state = State::PcInstall;
|
||||
return status;
|
||||
}
|
||||
// An update's own reports outrank what this session last said: one started here (the session
|
||||
// says Updating until the update writes its first step), or one this game was reopened after.
|
||||
const bool followUpdate = !g_sessionStateSet || g_sessionState == State::Updating;
|
||||
if (followUpdate && g_haveFileState) {
|
||||
status.state = g_fileState;
|
||||
status.detail = g_fileDetail;
|
||||
} else if (g_sessionStateSet) {
|
||||
status.state = g_sessionState;
|
||||
status.detail = g_sessionDetail;
|
||||
} else {
|
||||
status.state = State::Idle;
|
||||
}
|
||||
return status;
|
||||
}
|
||||
|
||||
void StartCheck() {
|
||||
{
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
if (g_sessionStateSet && (g_sessionState == State::Checking || g_sessionState == State::Updating)) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const Config config = ReadConfig();
|
||||
if (!config.Usable()) {
|
||||
return;
|
||||
}
|
||||
SetSessionState(State::Checking, "Asking github.com for the newest release");
|
||||
|
||||
// Detached: it talks to the network and must not hold up a frame, and there is no shutdown
|
||||
// ordering to manage because it touches nothing the game owns.
|
||||
std::thread([config] {
|
||||
const auto statusFile = RuntimeConfigFile::ApplicationDataDirectory() / kCheckStatusFileName;
|
||||
std::error_code ec;
|
||||
std::filesystem::remove(statusFile, ec);
|
||||
std::string command = "WIICOMPILED_UPDATE_STATUS=" + ShellQuoted(statusFile.string()) + ' ' +
|
||||
ShellQuoted(config.script.string()) + " --check";
|
||||
if (!config.workDir.empty()) {
|
||||
command += " --work-dir " + ShellQuoted(config.workDir.string());
|
||||
}
|
||||
command += " 2>&1";
|
||||
std::string lastLine;
|
||||
const int exitStatus = Run(command, lastLine);
|
||||
|
||||
State state = State::Failed;
|
||||
std::string detail;
|
||||
if (ReadStatusFile(statusFile, state, detail)) {
|
||||
SetSessionState(state, detail);
|
||||
} else if (exitStatus == 0) {
|
||||
// It ran and said nothing this can read, which is still an answer: ask again later.
|
||||
SetSessionState(State::Idle, "The check said nothing this version understands");
|
||||
} else {
|
||||
std::cerr << "[update] the check failed (exit " << exitStatus << "): " << lastLine << std::endl;
|
||||
SetSessionState(State::Failed,
|
||||
lastLine.empty() ? "The check could not be run" : lastLine);
|
||||
}
|
||||
std::filesystem::remove(statusFile, ec);
|
||||
}).detach();
|
||||
}
|
||||
|
||||
bool StartUpdate(std::string& error) {
|
||||
error.clear();
|
||||
const Config config = ReadConfig();
|
||||
if (!config.Usable()) {
|
||||
error = "This build was installed from a PC, so the update is run there.";
|
||||
return false;
|
||||
}
|
||||
|
||||
const auto dataDirectory = RuntimeConfigFile::ApplicationDataDirectory();
|
||||
std::error_code ec;
|
||||
std::filesystem::create_directories(dataDirectory, ec);
|
||||
// How the update opens the game again when it is done. Steam names the running title; without
|
||||
// one (a build started from a terminal) the update still runs and the game is started by hand.
|
||||
std::string steamGameId;
|
||||
for (const char* variable : {"SteamGameId", "SteamAppId"}) {
|
||||
if (const char* value = std::getenv(variable); value != nullptr && *value != '\0') {
|
||||
steamGameId = value;
|
||||
break;
|
||||
}
|
||||
}
|
||||
{
|
||||
std::ofstream request(dataDirectory / kRequestFileName, std::ios::trunc);
|
||||
if (!request) {
|
||||
error = "Could not write to " + dataDirectory.string();
|
||||
return false;
|
||||
}
|
||||
request << "# Written by the game's Update button; the update removes it.\n";
|
||||
if (!steamGameId.empty()) {
|
||||
request << "steam_game_id=" << steamGameId << '\n';
|
||||
}
|
||||
}
|
||||
// The previous update's result must not be read as this one's before it has written anything.
|
||||
std::filesystem::remove(dataDirectory / kStatusFileName, ec);
|
||||
// The update must outlive the game if the player closes it meanwhile, and Steam ends the game's
|
||||
// own processes when it closes, so it runs as a service of its own. --no-block: started, not
|
||||
// waited for.
|
||||
std::string lastLine;
|
||||
const std::string command =
|
||||
"systemctl --user start --no-block " + ShellQuoted(config.service) + " 2>&1";
|
||||
if (const int exitStatus = Run(command, lastLine); exitStatus != 0) {
|
||||
std::filesystem::remove(dataDirectory / kRequestFileName, ec);
|
||||
std::cerr << "[update] could not start " << config.service << " (exit " << exitStatus
|
||||
<< "): " << lastLine << std::endl;
|
||||
error = lastLine.empty() ? "Could not start the update service" : lastLine;
|
||||
return false;
|
||||
}
|
||||
SetSessionState(State::Updating, "Starting the update");
|
||||
// Read the files again at once, rather than show the removed status for up to a second.
|
||||
const std::lock_guard<std::mutex> lock(g_mutex);
|
||||
g_haveRead = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace update_check
|
||||
|
||||
#undef MKW_UPDATE_CHECK_SUPPORTED
|
||||
Reference in new issue
Block a user