mirror of
https://github.com/InstanceBrick/nightcap.git
synced 2026-10-23 07:01:01 +02:00
1 parent
e161fecfc5
commit
4b437f623f
15 files changed
+453
-8
No files matched your search
@@ -224,6 +224,7 @@ add_library(mocktail_update STATIC
|
||||
src/update/payload_integrity.cc
|
||||
src/update/payload_store.cc
|
||||
src/update/readiness_canary.cc
|
||||
src/update/unsafe_latest_runner.cc
|
||||
src/update/update_config.cc
|
||||
src/update/update_coordinator.cc
|
||||
src/update/zip_archive.cc
|
||||
|
||||
@@ -30,6 +30,9 @@ struct CommandLineOptions {
|
||||
std::string launch_request_json;
|
||||
int launch_argument_index = -1;
|
||||
bool allow_unverified_build = false;
|
||||
// Requests one explicit launch of the provider latest without promoting it
|
||||
// into the managed payload state.
|
||||
bool force_run_latest = false;
|
||||
};
|
||||
|
||||
struct CommandLineParseResult {
|
||||
|
||||
@@ -17,7 +17,8 @@ struct PayloadUpdatePreflightResult {
|
||||
};
|
||||
|
||||
PayloadUpdatePreflightResult RunPayloadUpdatePreflight(
|
||||
const Environment& environment, const RuntimePaths& paths);
|
||||
const Environment& environment, const RuntimePaths& paths,
|
||||
bool force_run_latest = false);
|
||||
|
||||
} // namespace runtime
|
||||
} // namespace mocktail
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
#ifndef MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_
|
||||
#define MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_
|
||||
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace mocktail::update {
|
||||
|
||||
// Launches a downloaded latest payload once without promoting it to the active
|
||||
// store. A non-empty HostAbi profile puts the child in explicit candidate mode;
|
||||
// normal startup approval is intentionally not created or modified.
|
||||
struct UnsafeLatestRunOptions {
|
||||
std::filesystem::path runtime_binary;
|
||||
std::filesystem::path payload_directory;
|
||||
std::filesystem::path compatibility_manifest;
|
||||
std::filesystem::path host_abi_profile;
|
||||
|
||||
// Tests may supply a deterministic environment. Production callers leave
|
||||
// this empty to inherit the updater process environment.
|
||||
std::vector<std::string> inherited_environment;
|
||||
};
|
||||
|
||||
struct UnsafeLatestRunResult {
|
||||
int exit_code = -1;
|
||||
std::string error;
|
||||
|
||||
explicit operator bool() const { return error.empty() && exit_code == 0; }
|
||||
};
|
||||
|
||||
UnsafeLatestRunResult RunUnsafeLatestCandidate(
|
||||
const UnsafeLatestRunOptions& options);
|
||||
|
||||
} // namespace mocktail::update
|
||||
|
||||
#endif // MOCKTAIL_UPDATE_UNSAFE_LATEST_RUNNER_H_
|
||||
@@ -24,6 +24,9 @@ struct UpdateRequest {
|
||||
bool startup_preflight = false;
|
||||
bool check_latest = true;
|
||||
bool run_canary = true;
|
||||
// Explicit manual escape hatch. It launches the provider latest once with a
|
||||
// derived candidate profile and never promotes it into current.json.
|
||||
bool force_run_latest = false;
|
||||
CanaryGraphicsBackend canary_graphics_backend =
|
||||
CanaryGraphicsBackend::kDirectVulkan;
|
||||
int progress_fd = -1;
|
||||
|
||||
+11
@@ -139,6 +139,17 @@ int main(int argc, char* argv[]) {
|
||||
MOCKTAIL_DEFAULT_COMPATIBILITY_MANIFEST));
|
||||
const mocktail::runtime::RuntimePaths paths =
|
||||
mocktail::runtime::RuntimePaths::FromEnvironment(environment);
|
||||
if (command_line.options.force_run_latest) {
|
||||
std::cerr << "[runtime] WARNING: latest Roblox will run once without "
|
||||
"approval and will not become the active payload\n";
|
||||
const mocktail::runtime::PayloadUpdatePreflightResult force_latest =
|
||||
mocktail::runtime::RunPayloadUpdatePreflight(environment, paths, true);
|
||||
if (!force_latest) {
|
||||
std::cerr << "[FATAL] " << force_latest.error << '\n';
|
||||
return EXIT_FAILURE;
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
std::optional<mocktail::runtime::RobloxExperienceLaunchRequest>
|
||||
external_launch_request;
|
||||
if (!command_line.options.launch_request_json.empty()) {
|
||||
|
||||
@@ -76,6 +76,8 @@ CommandLineParseResult ParseCommandLine(int argc, const char* const argv[]) {
|
||||
}
|
||||
} else if (argument == "--allow-unverified-build") {
|
||||
result.options.allow_unverified_build = true;
|
||||
} else if (argument == "--force-run-latest") {
|
||||
result.options.force_run_latest = true;
|
||||
} else if (argument == "--launch-uri") {
|
||||
if (!result.options.launch_request_json.empty()) {
|
||||
result.error = "duplicate option: --launch-uri";
|
||||
@@ -135,6 +137,14 @@ CommandLineParseResult ParseCommandLine(int argc, const char* const argv[]) {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
if (result.options.force_run_latest &&
|
||||
(!result.options.roblox_library_path.empty() ||
|
||||
result.options.window_mode != WindowMode::kUnspecified ||
|
||||
!result.options.graphics_backend.empty() ||
|
||||
result.options.allow_unverified_build ||
|
||||
!result.options.launch_request_json.empty())) {
|
||||
result.error = "--force-run-latest must be used on its own";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -242,6 +252,8 @@ std::string CommandLineUsage(const std::string& program_name) {
|
||||
"angle-vulkan (default: direct-vulkan)\n"
|
||||
<< " --allow-unverified-build Run a known but unverified Build-ID "
|
||||
"profile\n"
|
||||
<< " --force-run-latest Download and run the provider latest once "
|
||||
"without approval; it is not activated\n"
|
||||
<< " --launch-uri <uri> Join from a roblox: or roblox-player: "
|
||||
"website link\n"
|
||||
<< " --help, -h Show this help\n\n"
|
||||
|
||||
@@ -249,15 +249,20 @@ std::vector<std::string> ChildEnvironment(const RuntimePaths& paths,
|
||||
} // namespace
|
||||
|
||||
PayloadUpdatePreflightResult RunPayloadUpdatePreflight(
|
||||
const Environment& environment, const RuntimePaths& paths) {
|
||||
const Environment& environment, const RuntimePaths& paths,
|
||||
bool force_run_latest) {
|
||||
PayloadUpdatePreflightResult result;
|
||||
if (Enabled(environment, "MOCKTAIL_SKIP_UPDATE_CHECK") ||
|
||||
environment.HasNonEmpty("ROBLOX_LIB_PATH")) {
|
||||
if (!force_run_latest &&
|
||||
(Enabled(environment, "MOCKTAIL_SKIP_UPDATE_CHECK") ||
|
||||
environment.HasNonEmpty("ROBLOX_LIB_PATH"))) {
|
||||
return result;
|
||||
}
|
||||
|
||||
const std::filesystem::path helper = UpdateHelper(environment);
|
||||
if (helper.empty()) {
|
||||
if (force_run_latest) {
|
||||
result.error = "cannot locate the Mocktail update helper";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -265,7 +270,9 @@ PayloadUpdatePreflightResult RunPayloadUpdatePreflight(
|
||||
std::string helper_string = helper.string();
|
||||
char update[] = "update";
|
||||
char startup_preflight[] = "--startup-preflight";
|
||||
char* arguments[] = {helper_string.data(), update, startup_preflight,
|
||||
char force_latest[] = "--force-run-latest";
|
||||
char* arguments[] = {helper_string.data(), update,
|
||||
force_run_latest ? force_latest : startup_preflight,
|
||||
nullptr};
|
||||
UpdateProgressMonitor progress = UpdateProgressMonitor::Start(environment);
|
||||
std::vector<std::string> child_environment =
|
||||
@@ -318,7 +325,8 @@ PayloadUpdatePreflightResult RunPayloadUpdatePreflight(
|
||||
return result;
|
||||
}
|
||||
if (!WIFEXITED(child_status) || WEXITSTATUS(child_status) != 0) {
|
||||
result.error = "Roblox update preflight failed";
|
||||
result.error = force_run_latest ? "forced latest Roblox run failed"
|
||||
: "Roblox update preflight failed";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -311,11 +311,23 @@ int main(int argc, char** argv) {
|
||||
request.check_latest = false;
|
||||
} else if (option == "--skip-canary") {
|
||||
request.run_canary = false;
|
||||
} else if (option == "--force-run-latest") {
|
||||
request.force_run_latest = true;
|
||||
} else {
|
||||
std::cerr << "[native-updater] unknown option: " << option << '\n';
|
||||
return 2;
|
||||
}
|
||||
}
|
||||
if (request.force_run_latest &&
|
||||
(request.startup_preflight || !request.check_latest || !request.run_canary)) {
|
||||
std::cerr << "[native-updater] --force-run-latest cannot be combined with "
|
||||
"--startup-preflight, --no-latest-check, or --skip-canary\n";
|
||||
return 2;
|
||||
}
|
||||
if (request.force_run_latest) {
|
||||
std::cerr << "[native-updater] WARNING: latest Roblox will run once without "
|
||||
"approval and will not become the active payload\n";
|
||||
}
|
||||
const mocktail::update::UpdateResult updated =
|
||||
mocktail::update::RunUpdate(paths, request);
|
||||
for (const std::string& warning : updated.warnings) {
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
#include "update/unsafe_latest_runner.h"
|
||||
|
||||
#include <spawn.h>
|
||||
#include <sys/wait.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <array>
|
||||
#include <cerrno>
|
||||
#include <cstring>
|
||||
#include <filesystem>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
extern char** environ;
|
||||
|
||||
namespace mocktail::update {
|
||||
namespace {
|
||||
|
||||
constexpr std::array<std::string_view, 10> kOverriddenVariables = {
|
||||
"ROBLOX_LIB_PATH",
|
||||
"MOCKTAIL_ASSET_ROOT",
|
||||
"MOCKTAIL_ASSET_PATH",
|
||||
"MOCKTAIL_COMPATIBILITY_MANIFEST",
|
||||
"MOCKTAIL_HOST_ABI_PROFILE_FILE",
|
||||
"MOCKTAIL_HOST_ABI_CANARY",
|
||||
"MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI",
|
||||
"MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT",
|
||||
"MOCKTAIL_SKIP_UPDATE_CHECK",
|
||||
"MOCKTAIL_UNSAFE_LATEST",
|
||||
};
|
||||
|
||||
bool IsEnvironmentEntry(std::string_view entry, std::string_view name) {
|
||||
return entry.size() > name.size() && entry[name.size()] == '=' &&
|
||||
entry.substr(0, name.size()) == name;
|
||||
}
|
||||
|
||||
bool IsOverridden(std::string_view entry) {
|
||||
for (const std::string_view name : kOverriddenVariables) {
|
||||
if (IsEnvironmentEntry(entry, name)) return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool IsRegularFile(const std::filesystem::path& path) {
|
||||
std::error_code error;
|
||||
const std::filesystem::file_status status =
|
||||
std::filesystem::symlink_status(path, error);
|
||||
return !error && std::filesystem::is_regular_file(status) &&
|
||||
!std::filesystem::is_symlink(status);
|
||||
}
|
||||
|
||||
bool IsExecutableRegularFile(const std::filesystem::path& path) {
|
||||
return IsRegularFile(path) && access(path.c_str(), X_OK) == 0;
|
||||
}
|
||||
|
||||
std::vector<std::string> CurrentEnvironment() {
|
||||
std::vector<std::string> result;
|
||||
for (char** entry = environ; entry != nullptr && *entry != nullptr; ++entry) {
|
||||
result.emplace_back(*entry);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> BuildEnvironment(
|
||||
const UnsafeLatestRunOptions& options) {
|
||||
const std::vector<std::string> inherited =
|
||||
options.inherited_environment.empty() ? CurrentEnvironment()
|
||||
: options.inherited_environment;
|
||||
std::vector<std::string> result;
|
||||
result.reserve(inherited.size() + 10U);
|
||||
for (const std::string& entry : inherited) {
|
||||
if (entry.find('\0') == std::string::npos && !IsOverridden(entry)) {
|
||||
result.push_back(entry);
|
||||
}
|
||||
}
|
||||
const auto add = [&](std::string_view name,
|
||||
const std::filesystem::path& value) {
|
||||
result.push_back(std::string(name) + "=" + value.string());
|
||||
};
|
||||
add("ROBLOX_LIB_PATH", options.payload_directory / "libroblox.so");
|
||||
add("MOCKTAIL_ASSET_ROOT", options.payload_directory / "assets");
|
||||
add("MOCKTAIL_ASSET_PATH", options.payload_directory / "assets/content");
|
||||
add("MOCKTAIL_COMPATIBILITY_MANIFEST", options.compatibility_manifest);
|
||||
result.push_back("MOCKTAIL_SKIP_UPDATE_CHECK=1");
|
||||
result.push_back("MOCKTAIL_UNSAFE_LATEST=1");
|
||||
if (!options.host_abi_profile.empty()) {
|
||||
add("MOCKTAIL_HOST_ABI_PROFILE_FILE", options.host_abi_profile);
|
||||
result.push_back("MOCKTAIL_HOST_ABI_CANARY=1");
|
||||
result.push_back("MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1");
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
UnsafeLatestRunResult RunUnsafeLatestCandidate(
|
||||
const UnsafeLatestRunOptions& options) {
|
||||
UnsafeLatestRunResult result;
|
||||
if (!IsExecutableRegularFile(options.runtime_binary)) {
|
||||
result.error = "unsafe latest runtime is not an executable regular file";
|
||||
return result;
|
||||
}
|
||||
if (!IsRegularFile(options.payload_directory / "libroblox.so")) {
|
||||
result.error = "unsafe latest payload library is not a regular file";
|
||||
return result;
|
||||
}
|
||||
if (!IsRegularFile(options.compatibility_manifest)) {
|
||||
result.error = "unsafe latest compatibility manifest is not a regular file";
|
||||
return result;
|
||||
}
|
||||
if (!options.host_abi_profile.empty() &&
|
||||
!IsRegularFile(options.host_abi_profile)) {
|
||||
result.error = "unsafe latest HostAbi profile is not a regular file";
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<std::string> environment = BuildEnvironment(options);
|
||||
std::vector<char*> environment_pointers;
|
||||
environment_pointers.reserve(environment.size() + 1U);
|
||||
for (std::string& entry : environment)
|
||||
environment_pointers.push_back(entry.data());
|
||||
environment_pointers.push_back(nullptr);
|
||||
|
||||
std::string binary = options.runtime_binary.string();
|
||||
char windowed[] = "--windowed";
|
||||
char allow_unverified[] = "--allow-unverified-build";
|
||||
char* arguments[] = {
|
||||
binary.data(), windowed,
|
||||
options.host_abi_profile.empty() ? nullptr : allow_unverified, nullptr};
|
||||
pid_t child = -1;
|
||||
const int spawn_status =
|
||||
posix_spawn(&child, options.runtime_binary.c_str(), nullptr, nullptr,
|
||||
arguments, environment_pointers.data());
|
||||
if (spawn_status != 0) {
|
||||
result.error = "cannot start unsafe latest Roblox: " +
|
||||
std::string(std::strerror(spawn_status));
|
||||
return result;
|
||||
}
|
||||
|
||||
int status = 0;
|
||||
while (waitpid(child, &status, 0) < 0) {
|
||||
if (errno == EINTR) continue;
|
||||
result.error = "cannot wait for unsafe latest Roblox: " +
|
||||
std::string(std::strerror(errno));
|
||||
return result;
|
||||
}
|
||||
if (!WIFEXITED(status)) {
|
||||
result.exit_code = 128 + (WIFSIGNALED(status) ? WTERMSIG(status) : 0);
|
||||
result.error = "unsafe latest Roblox terminated before a normal exit";
|
||||
return result;
|
||||
}
|
||||
result.exit_code = WEXITSTATUS(status);
|
||||
if (result.exit_code != 0) {
|
||||
result.error = "unsafe latest Roblox exited with status " +
|
||||
std::to_string(result.exit_code);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
} // namespace mocktail::update
|
||||
@@ -22,6 +22,7 @@
|
||||
#include "update/payload_integrity.h"
|
||||
#include "update/payload_store.h"
|
||||
#include "update/readiness_canary.h"
|
||||
#include "update/unsafe_latest_runner.h"
|
||||
#include "update/update_config.h"
|
||||
|
||||
namespace mocktail::update {
|
||||
@@ -277,6 +278,109 @@ ReferenceProfile ResolveReference(const UpdatePaths& paths,
|
||||
return result;
|
||||
}
|
||||
|
||||
UpdateResult RunUnsafeLatest(
|
||||
const UpdatePaths& paths, const UpdateRequest& request,
|
||||
const std::vector<SupportedPayloadProfile>& profiles,
|
||||
const SupportedPayloadProfile& preferred,
|
||||
const PayloadStoreResult& installed, PayloadStore* store) {
|
||||
UpdateResult result;
|
||||
if (request.startup_preflight || !request.check_latest) {
|
||||
result.error =
|
||||
"unsafe latest launch requires an explicit latest-version check";
|
||||
return result;
|
||||
}
|
||||
|
||||
ApkPureProvider provider;
|
||||
Progress(request.progress_fd, "Checking latest Roblox...");
|
||||
const ProviderVersion latest = provider.CheckLatest();
|
||||
if (!latest) {
|
||||
result.error = "cannot check latest Roblox for unsafe launch: " +
|
||||
latest.error;
|
||||
return result;
|
||||
}
|
||||
|
||||
std::string workspace_error;
|
||||
const std::filesystem::path workspace = UniqueDirectory(
|
||||
paths.cache_root / "downloads/native-updater", &workspace_error);
|
||||
if (!workspace_error.empty()) {
|
||||
result.error = workspace_error;
|
||||
return result;
|
||||
}
|
||||
std::error_code filesystem_error;
|
||||
const auto cleanup = [&]() {
|
||||
std::filesystem::remove_all(workspace, filesystem_error);
|
||||
};
|
||||
|
||||
ExpectedPayloadIdentity identity;
|
||||
identity.version_name = latest.version_name;
|
||||
identity.version_code = latest.version_code;
|
||||
const std::optional<SupportedPayloadProfile> exact =
|
||||
FindSupportedProfile(profiles, identity.version_name, identity.version_code);
|
||||
if (exact.has_value()) identity.exact_build_id = exact->elf_build_id;
|
||||
Candidate candidate = FindStagedCandidate(paths, identity, exact.has_value());
|
||||
if (!candidate) {
|
||||
candidate = DownloadCandidate(&provider, store, identity, exact.has_value(),
|
||||
paths, workspace / "candidate",
|
||||
request.progress_fd);
|
||||
}
|
||||
if (!candidate) {
|
||||
result.error = "cannot prepare latest Roblox for unsafe launch: " +
|
||||
candidate.error;
|
||||
cleanup();
|
||||
return result;
|
||||
}
|
||||
|
||||
if (!candidate.exact_supported) {
|
||||
const ReferenceProfile reference = ResolveReference(
|
||||
paths, installed, preferred, &provider, store, workspace,
|
||||
request.progress_fd);
|
||||
if (!reference) {
|
||||
result.error = "cannot prepare latest Roblox for unsafe launch: " +
|
||||
reference.error;
|
||||
cleanup();
|
||||
return result;
|
||||
}
|
||||
HostAbiDerivationOptions derivation;
|
||||
derivation.reference_library = reference.library;
|
||||
derivation.reference_profile = reference.profile;
|
||||
derivation.candidate_payload_directory = candidate.staged.payload_directory;
|
||||
derivation.output_directory = workspace / "derived";
|
||||
Progress(request.progress_fd, "Deriving latest Roblox compatibility...");
|
||||
const HostAbiDerivationResult derived = DeriveHostAbiProfile(derivation);
|
||||
if (!derived) {
|
||||
result.error = "cannot derive latest Roblox HostAbi profile: " +
|
||||
derived.error;
|
||||
cleanup();
|
||||
return result;
|
||||
}
|
||||
candidate.profile = derived.profile;
|
||||
candidate.compatibility = derived.compatibility_manifest;
|
||||
}
|
||||
|
||||
// Staging preserves the immutable APK for inspection, but this path must
|
||||
// never promote it or alter the normal current payload.
|
||||
UnsafeLatestRunOptions launch;
|
||||
launch.runtime_binary = paths.runtime_binary;
|
||||
launch.payload_directory = candidate.staged.payload_directory;
|
||||
launch.compatibility_manifest = candidate.exact_supported
|
||||
? paths.compatibility_manifest
|
||||
: candidate.compatibility;
|
||||
launch.host_abi_profile = candidate.profile;
|
||||
Progress(request.progress_fd, "Launching unapproved latest Roblox...");
|
||||
const UnsafeLatestRunResult launched = RunUnsafeLatestCandidate(launch);
|
||||
result.payload_id = candidate.staged.payload_id;
|
||||
if (!launched) {
|
||||
result.error = "unsafe latest Roblox " + candidate.staged.version_name +
|
||||
" did not complete: " + launched.error;
|
||||
cleanup();
|
||||
return result;
|
||||
}
|
||||
result.message = "unsafe latest Roblox " + candidate.staged.version_name +
|
||||
" exited successfully; active payload was not changed";
|
||||
cleanup();
|
||||
return result;
|
||||
}
|
||||
|
||||
bool RunCandidateCanaries(const UpdatePaths& paths, const Candidate& candidate,
|
||||
bool run_canary,
|
||||
CanaryGraphicsBackend graphics_backend,
|
||||
@@ -383,6 +487,12 @@ UpdateResult RunUpdate(const UpdatePaths& paths, const UpdateRequest& request) {
|
||||
} else if (!current) {
|
||||
installed = store.InspectCurrent();
|
||||
}
|
||||
if (request.force_run_latest) {
|
||||
UpdateResult unsafe = RunUnsafeLatest(paths, request, catalog.profiles,
|
||||
*preferred, installed, &store);
|
||||
unsafe.warnings = std::move(result.warnings);
|
||||
return unsafe;
|
||||
}
|
||||
if (!configured.config.automatic && request.startup_preflight) {
|
||||
if (current) {
|
||||
result.payload_id = current.payload_id;
|
||||
|
||||
@@ -267,7 +267,8 @@ TEST(CommandLineTest, UsageContainsEverySupportedOption) {
|
||||
EXPECT_NE(usage.find("mocktail-test"), std::string::npos);
|
||||
for (const char* option :
|
||||
{"--roblox-lib", "--headless", "--windowed", "--graphics",
|
||||
"--allow-unverified-build", "--launch-uri", "--help"}) {
|
||||
"--allow-unverified-build", "--force-run-latest", "--launch-uri",
|
||||
"--help"}) {
|
||||
EXPECT_NE(usage.find(option), std::string::npos) << option;
|
||||
}
|
||||
EXPECT_EQ(usage.find("--login"), std::string::npos);
|
||||
|
||||
@@ -28,7 +28,8 @@ int IntegerEnvironment(const char* name, int fallback) {
|
||||
|
||||
int main(int argc, char** argv) {
|
||||
if (argc != 3 || std::string_view(argv[1]) != "update" ||
|
||||
std::string_view(argv[2]) != "--startup-preflight") {
|
||||
(std::string_view(argv[2]) != "--startup-preflight" &&
|
||||
std::string_view(argv[2]) != "--force-run-latest")) {
|
||||
return 64;
|
||||
}
|
||||
const char* capture = Environment("MOCKTAIL_TEST_UPDATE_CAPTURE");
|
||||
@@ -40,6 +41,13 @@ int main(int argc, char** argv) {
|
||||
<< Environment("MOCKTAIL_STATE_ROOT") << '\n';
|
||||
if (!output) return 65;
|
||||
}
|
||||
const char* argument_capture =
|
||||
Environment("MOCKTAIL_TEST_UPDATE_ARGUMENT_CAPTURE");
|
||||
if (argument_capture[0] != '\0') {
|
||||
std::ofstream output(argument_capture);
|
||||
output << argv[2] << '\n';
|
||||
if (!output) return 67;
|
||||
}
|
||||
const int progress = IntegerEnvironment("MOCKTAIL_UPDATE_PROGRESS_FD", -1);
|
||||
if (progress >= 0) {
|
||||
constexpr std::string_view packet = "PDownloading Roblox...";
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <minizip/zip.h>
|
||||
#include <sys/stat.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstdlib>
|
||||
@@ -16,6 +17,7 @@
|
||||
#include "update/payload_integrity.h"
|
||||
#include "update/payload_store.h"
|
||||
#include "update/readiness_canary.h"
|
||||
#include "update/unsafe_latest_runner.h"
|
||||
#include "update/update_config.h"
|
||||
#include "update/zip_archive.h"
|
||||
|
||||
@@ -161,6 +163,65 @@ TEST(ApkPureProviderTest, ParsesExactVersionIdentityAndTrustedUrl) {
|
||||
EXPECT_EQ(urls.front(), "https://download.pureapk.com/b/XAPK/roblox");
|
||||
}
|
||||
|
||||
TEST(UnsafeLatestRunnerTest, LaunchesCandidateWithUnapprovedEnvironment) {
|
||||
TemporaryDirectory temporary;
|
||||
const std::filesystem::path output = temporary.root() / "environment.txt";
|
||||
const std::filesystem::path arguments = temporary.root() / "arguments.txt";
|
||||
const std::filesystem::path runtime = temporary.root() / "runtime.sh";
|
||||
Write(runtime,
|
||||
"#!/bin/sh\n"
|
||||
"env | sort > \"$ENV_OUTPUT\"\n"
|
||||
"printf '%s\\n' \"$@\" > \"$ARGUMENT_OUTPUT\"\n");
|
||||
ASSERT_EQ(chmod(runtime.c_str(), 0700), 0);
|
||||
|
||||
const std::filesystem::path payload = temporary.root() / "payload";
|
||||
Write(payload / "libroblox.so", "candidate");
|
||||
std::filesystem::create_directories(payload / "assets/content");
|
||||
const std::filesystem::path compatibility =
|
||||
temporary.root() / "compatibility.json";
|
||||
const std::filesystem::path profile = temporary.root() / "profile.json";
|
||||
Write(compatibility, "{}\n");
|
||||
Write(profile, "{}\n");
|
||||
|
||||
UnsafeLatestRunOptions options;
|
||||
options.runtime_binary = runtime;
|
||||
options.payload_directory = payload;
|
||||
options.compatibility_manifest = compatibility;
|
||||
options.host_abi_profile = profile;
|
||||
options.inherited_environment = {
|
||||
"PATH=/usr/bin:/bin", "ENV_OUTPUT=" + output.string(),
|
||||
"ARGUMENT_OUTPUT=" + arguments.string(), "UNCHANGED=value",
|
||||
"ROBLOX_LIB_PATH=/stale/libroblox.so",
|
||||
"MOCKTAIL_HOST_ABI_APPROVAL_RECEIPT=/stale/approval.json",
|
||||
"MOCKTAIL_SKIP_UPDATE_CHECK=0",
|
||||
};
|
||||
|
||||
const UnsafeLatestRunResult result = RunUnsafeLatestCandidate(options);
|
||||
ASSERT_TRUE(result) << result.error;
|
||||
const std::string environment = ReadFile(output);
|
||||
EXPECT_NE(environment.find("UNCHANGED=value\n"), std::string::npos);
|
||||
EXPECT_NE(environment.find("ROBLOX_LIB_PATH=" +
|
||||
(payload / "libroblox.so").string() + "\n"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(environment.find("MOCKTAIL_COMPATIBILITY_MANIFEST=" +
|
||||
compatibility.string() + "\n"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(environment.find("MOCKTAIL_HOST_ABI_PROFILE_FILE=" +
|
||||
profile.string() + "\n"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(environment.find("MOCKTAIL_HOST_ABI_CANARY=1\n"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(environment.find("MOCKTAIL_ALLOW_CANDIDATE_HOST_ABI=1\n"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(environment.find("MOCKTAIL_SKIP_UPDATE_CHECK=1\n"),
|
||||
std::string::npos);
|
||||
EXPECT_NE(environment.find("MOCKTAIL_UNSAFE_LATEST=1\n"),
|
||||
std::string::npos);
|
||||
EXPECT_EQ(environment.find("/stale/libroblox.so"), std::string::npos);
|
||||
EXPECT_EQ(environment.find("/stale/approval.json"), std::string::npos);
|
||||
EXPECT_EQ(ReadFile(arguments), "--windowed\n--allow-unverified-build\n");
|
||||
}
|
||||
|
||||
TEST(HttpDownloadPolicyTest, RejectsCredentialsAndHostSuffixTricks) {
|
||||
const std::vector<std::string> hosts = {"pureapk.com"};
|
||||
EXPECT_TRUE(IsTrustedHttpsUrl("https://download.pureapk.com/a", hosts));
|
||||
|
||||
@@ -101,6 +101,22 @@ TEST(PayloadUpdatePreflightTest, RunsNativeUpdater) {
|
||||
EXPECT_TRUE(result.attempted);
|
||||
}
|
||||
|
||||
TEST(PayloadUpdatePreflightTest, RunsForceLatestThroughNativeUpdater) {
|
||||
TemporaryDirectory temporary;
|
||||
const std::filesystem::path capture = temporary.root() / "argument";
|
||||
ScopedVariable capture_variable("MOCKTAIL_TEST_UPDATE_ARGUMENT_CAPTURE",
|
||||
capture.string());
|
||||
const MapEnvironment environment = NativeEnvironment();
|
||||
const RuntimePaths paths = RuntimePaths::FromEnvironment(environment);
|
||||
|
||||
const auto result = RunPayloadUpdatePreflight(environment, paths, true);
|
||||
|
||||
ASSERT_TRUE(result) << result.error;
|
||||
EXPECT_TRUE(result.attempted);
|
||||
EXPECT_EQ(ReadLines(capture),
|
||||
std::vector<std::string>({"--force-run-latest"}));
|
||||
}
|
||||
|
||||
TEST(PayloadUpdatePreflightTest, RunsPackagedUpdaterFromProjectBin) {
|
||||
TemporaryDirectory temporary;
|
||||
const std::filesystem::path project_root = temporary.root() / "portable";
|
||||
|
||||
Reference in new issue
Block a user