fix(webview): allow 5 s for helper startup and log its timing

Slow starts (cold Flatpak) could exceed the 3 s ready deadline and kill
login verification windows. Launch and timeout logs now report elapsed ms.
This commit is contained in:
vii committed 2026-09-29 16:05:01 -05:00
1 parent 5f83c4a64f
commit 9b041ec2ac
2 files changed
+66 -6

No files matched your search

+15 -5
View File
@@ -22,7 +22,8 @@ namespace {
constexpr size_t kMaxPendingLaunchRequests = 8;
// libroblox's static TLS requires the proven 16 MiB guest-thread stack floor.
constexpr size_t kLaunchWorkerStackSize = 64ULL * 1024 * 1024;
constexpr std::chrono::milliseconds kWebSurfaceReadyTimeout{3000};
// Covers helper exec through window creation, including cold Flatpak starts.
constexpr std::chrono::milliseconds kWebSurfaceReadyTimeout{5000};
constexpr char kRobloxBaseUrl[] = "https://www.roblox.com/";
Status Invalid(std::string message) {
@@ -159,24 +160,33 @@ Status LaunchRobloxWebSurface(
}
std::fprintf(stderr, " [%s] opening validated Roblox web surface\n",
transport);
const auto started = std::chrono::steady_clock::now();
const WebViewHelperLaunchResult launched =
LaunchWebViewHelper(helper, url, std::move(exit_observer));
if (!launched) {
return Unavailable("could not display Roblox web surface: " +
launched.error);
}
if (launched.process == nullptr ||
!launched.process->WaitUntilReady(kWebSurfaceReadyTimeout)) {
const bool ready = launched.process != nullptr &&
launched.process->WaitUntilReady(kWebSurfaceReadyTimeout);
const long long elapsed_ms =
std::chrono::duration_cast<std::chrono::milliseconds>(
std::chrono::steady_clock::now() - started)
.count();
if (!ready) {
if (launched.process != nullptr) {
(void)launched.process->RequestClose();
}
return Unavailable(
"Roblox web surface did not become ready before the deadline");
"Roblox web surface did not become ready before the deadline (" +
std::to_string(elapsed_ms) + " ms of " +
std::to_string(kWebSurfaceReadyTimeout.count()) + " ms)");
}
if (process != nullptr) {
*process = launched.process;
}
std::fprintf(stderr, " [%s] Roblox web surface launched\n", transport);
std::fprintf(stderr, " [%s] Roblox web surface launched in %lld ms\n",
transport, elapsed_ms);
return Status::Ok();
}
+51 -1
View File
@@ -1023,7 +1023,7 @@ TEST(RobloxExperienceCompositionTest,
class RobloxExperienceCompositionWebSurfaceTest : public ::testing::Test {
protected:
struct Helper {
Helper() {
explicit Helper(const char* before_ready = "") {
char pattern[] = "/tmp/mocktail_surface_close_XXXXXX";
const char* created = mkdtemp(pattern);
if (created == nullptr) return;
@@ -1035,6 +1035,7 @@ import socket, sys
sys.stdin.buffer.read()
s = socket.socket(fileno=198)
s.settimeout(5)
)PY" << before_ready << R"PY(
s.send(b'MWVE' + bytes([1, 3, 0, 0]) + bytes(4))
while True:
packet = s.recv(1024 * 1024)
@@ -1071,6 +1072,14 @@ while True:
++static_cast<ExitProbe*>(context)->calls;
}
static Status Open(RobloxExperienceComposition* composition,
const std::shared_ptr<ExitProbe>& probe) {
return composition->OpenWebSurface(
"https://www.roblox.com/challenge/cdn/hybrid", "webview",
RobloxExperienceComposition::WebSurfaceRoute::kWebView,
{probe, &RobloxExperienceCompositionWebSurfaceTest::ObserveExit}, {});
}
static std::unique_ptr<RobloxExperienceComposition> MakeComposition(
const SecureRobloxCredential* credential = nullptr) {
return std::make_unique<RobloxExperienceComposition>(
@@ -1278,6 +1287,47 @@ TEST_F(RobloxExperienceCompositionWebSurfaceTest,
EXPECT_EQ(probe->calls, 1);
}
TEST_F(RobloxExperienceCompositionWebSurfaceTest,
WaitsForSlowHelperStartupBeforeRejectingSurface) {
Helper helper("import time\ntime.sleep(3.5)\n");
ASSERT_FALSE(helper.path.empty());
ASSERT_EQ(setenv("MOCKTAIL_WEBVIEW_HELPER", helper.path.c_str(), 1), 0);
SecureRobloxCredential credential{".ROBLOSECURITY=typed-secret"};
auto composition = MakeComposition(&credential);
auto probe = std::make_shared<ExitProbe>();
::testing::internal::CaptureStderr();
const Status status = Open(composition.get(), probe);
const std::string log = ::testing::internal::GetCapturedStderr();
unsetenv("MOCKTAIL_WEBVIEW_HELPER");
EXPECT_TRUE(status.ok()) << status.message();
const std::string marker = "Roblox web surface launched in ";
const size_t at = log.find(marker);
ASSERT_NE(at, std::string::npos) << log;
EXPECT_GE(std::stol(log.substr(at + marker.size())), 3500) << log;
EXPECT_TRUE(Close(composition.get()).ok());
}
TEST_F(RobloxExperienceCompositionWebSurfaceTest,
ReportsElapsedStartupTimeWhenHelperNeverBecomesReady) {
Helper helper("sys.exit(0)\n");
ASSERT_FALSE(helper.path.empty());
ASSERT_EQ(setenv("MOCKTAIL_WEBVIEW_HELPER", helper.path.c_str(), 1), 0);
SecureRobloxCredential credential{".ROBLOSECURITY=typed-secret"};
auto composition = MakeComposition(&credential);
auto probe = std::make_shared<ExitProbe>();
const Status status = Open(composition.get(), probe);
unsetenv("MOCKTAIL_WEBVIEW_HELPER");
ASSERT_FALSE(status.ok());
EXPECT_NE(status.message().find("did not become ready"), std::string::npos)
<< status.message();
EXPECT_NE(status.message().find(" ms of 5000 ms"), std::string::npos)
<< status.message();
}
namespace {} // namespace
} // namespace runtime
} // namespace mocktail