From d8d48d8651abf37dd31e7f1c222d4b7c5ac55760 Mon Sep 17 00:00:00 2001 From: Paul Hitt Date: Sat, 5 Sep 2026 15:41:49 -0400 Subject: [PATCH] =?UTF-8?q?"Checking=20what=20this=20server=20uses?= =?UTF-8?q?=E2=80=A6"=20was=20also=20the=20failure=20state?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit πŸ”΄ **The relay probe had no deadline and no way to say it failed.** `discovered` is null both while the probe is in flight and after it has come back with nothing, so `identityDescription` shows "Checking what this server uses…" in both cases. A relay that never answers leaves the sign-up form on that sentence permanently β€” no error, no retry, nothing to act on. Found by the instrumented suite on a clean emulator: nine of ten tests failed with `nav-contacts did not appear within 120000ms`, and the screen dump showed the form still checking. ⚠️ **The reason it never answered is worth writing down.** The app was pointed at `http://10.0.2.2:18810`, and Android has blocked cleartext HTTP by default since API 28 β€” there is no `usesCleartextTraffic` and no network security config here. The request never left the device. πŸ”΄ That refusal is CORRECT and is not what changed. `SiteConfig` already says why: a bare hostname becoming `http://` would hand key material to anyone on the path "and it would work, which is the worst outcome." What was wrong is that it failed silently, so somebody self-hosting who omits the scheme gets a dead screen and no idea why. Now: bounded at fifteen seconds β€” far longer than a healthy relay needs β€” and a failed probe is a distinct state that says "Couldn't reach that server. Check the address β€” and note it has to be https, not http." Same defect as android#19 one screen earlier, which I fixed this morning without noticing the probe had it too. Also adds `scripts/instrumented.sh`, which closes two ways an on-device run silently points at production: the suites DEFAULT to live hosts when no relay is named, and a saved emulator snapshot restores whatever the AVD was last doing β€” observed today as a snapshot signed in as `fnreviewbeta` against relay.flamenet.io, under a run told to use a local relay. The script has no default, refuses both live hosts by name, cold boots with `-no-snapshot-load`, and exits 3 on a skip because a skip is not a pass. --- .../messenger/app/MessengerViewModel.kt | 20 +++- .../io/flamenet/messenger/app/SiteConfig.kt | 15 +++ scripts/instrumented.sh | 98 +++++++++++++++++++ 3 files changed, 131 insertions(+), 2 deletions(-) create mode 100755 scripts/instrumented.sh diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt index 0efdbc7..b40a6c8 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/MessengerViewModel.kt @@ -339,10 +339,23 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) { describeJob = viewModelScope.launch { kotlinx.coroutines.delay(400) val config = SiteConfig.from(value) ?: return@launch - val found = withContext(Dispatchers.IO) { - io.flamenet.client.RelayDiscovery.describe(config.baseUrl) + // ⚠️ **Bounded.** `describe` has no deadline of its own, and a + // host that accepts a connection and never answers would leave the + // sign-up form on "Checking what this server uses…" indefinitely. + // Fifteen seconds is far longer than any healthy relay needs. + val found = withTimeoutOrNull(RELAY_PROBE_TIMEOUT_MS) { + withContext(Dispatchers.IO) { + io.flamenet.client.RelayDiscovery.describe(config.baseUrl) + } } if (_relayInput.value != value) return@launch + if (found == null) { + // Say so rather than leaving the spinner-in-words up. The + // commonest cause is an http:// address, which Android refuses + // outright, so the message names it. + _site.value = config.copy(probeFailed = true) + return@launch + } // copy(), not a mutation: SiteConfig is a data class and the flow // conflates on equality, so an in-place write is indistinguishable // from the value already there and never reaches the UI. @@ -3570,6 +3583,9 @@ class MessengerViewModel(app: Application) : AndroidViewModel(app) { */ private const val SIGN_IN_TIMEOUT_MS = 60_000L + /** How long to wait for a relay to describe itself before saying it did not. */ + private const val RELAY_PROBE_TIMEOUT_MS = 15_000L + /** * The presence chips, in order. * diff --git a/app/src/main/kotlin/io/flamenet/messenger/app/SiteConfig.kt b/app/src/main/kotlin/io/flamenet/messenger/app/SiteConfig.kt index 54e29e1..b46dbff 100644 --- a/app/src/main/kotlin/io/flamenet/messenger/app/SiteConfig.kt +++ b/app/src/main/kotlin/io/flamenet/messenger/app/SiteConfig.kt @@ -29,6 +29,18 @@ data class SiteConfig( * it with `copy(...)`. */ val discovered: io.flamenet.client.RelayDiscovery.Descriptor? = null, + /** + * The probe was made and came back with nothing. + * + * πŸ”΄ **Without this, "still asking" and "asked and could not reach it" are + * the same state.** `discovered` is null in both, so a relay that never + * answers leaves "Checking what this server uses…" on screen forever and + * the form permanently unusable β€” no error, no retry, nothing to act on. + * Seen 2026-09-05 pointing the app at a plain-HTTP relay, which Android + * blocks outright from API 28: the request never left, and the screen said + * it was still thinking about it. + */ + val probeFailed: Boolean = false, ) { enum class AccountProvider { @@ -95,6 +107,9 @@ data class SiteConfig( /** What to tell the user this relay does about identity, in their words. */ val identityDescription: String get() = when { + probeFailed -> + "Couldn't reach that server. Check the address β€” and note it has " + + "to be https, not http." discovered == null -> "Checking what this server uses…" accountProvider == AccountProvider.SHARED -> "One Flamenet account, shared with Flamenet Files." diff --git a/scripts/instrumented.sh b/scripts/instrumented.sh new file mode 100755 index 0000000..785c825 --- /dev/null +++ b/scripts/instrumented.sh @@ -0,0 +1,98 @@ +#!/usr/bin/env bash +# Run the on-device suites against a relay you named, on an emulator with no +# history. +# +# πŸ”΄ **Two ways an instrumented run silently points at PRODUCTION, and this +# script closes both.** +# +# 1. The suites DEFAULT to a live host when no relay argument is passed β€” +# `FeatureUseTest` to relay1.flamenet.io, `LiveSignUpFleetTest` to +# relay.flamenet.io. A plain `./gradlew connectedAndroidTest` therefore +# signs accounts up on somebody's real server, and nothing in the output +# says so. +# 2. A SAVED EMULATOR SNAPSHOT restores whatever the AVD was last doing. +# Observed 2026-09-05: a snapshot signed in as `fnreviewbeta` against +# relay.flamenet.io, restored under a test run that had been told to use a +# local relay. Every one of the ten tests failed β€” they could not find the +# sign-up screen, because the app was already signed in β€” and the reason +# was invisible until the failure's screen dump was read. +# +# Usage: scripts/instrumented.sh 10.0.2.2:18810 [ClassName ...] +set -euo pipefail + +RELAY="${1:-}" +[ -n "$RELAY" ] || { + cat >&2 <<'MSG' +usage: scripts/instrumented.sh [TestClass ...] + +Name the relay. There is deliberately no default: the suites have their own, +and theirs are live servers. + + 10.0.2.2:18810 the host machine's local relay, as seen from the emulator +MSG + exit 2 +} +shift || true + +# πŸ”΄ Refused rather than warned about. A run that creates accounts on the +# production relay cannot be undone by noticing afterwards. +case "$RELAY" in + *relay.flamenet.io*|*relay1.flamenet.io*) + echo "REFUSED: $RELAY is a live relay. These suites create accounts." >&2 + echo "Point them at a local relay, or edit this check if you truly mean it." >&2 + exit 1;; +esac + +CLASSES="${*:-io.flamenet.messenger.app.FeatureUseTest,io.flamenet.messenger.app.CrudSurfaceTest,io.flamenet.messenger.app.ChatFlowTest}" +CLASSES="${CLASSES// /,}" + +export ANDROID_HOME="${ANDROID_HOME:-$HOME/Library/Android/sdk}" +export JAVA_HOME="${JAVA_HOME:-/opt/homebrew/opt/openjdk@21}" +ADB="$ANDROID_HOME/platform-tools/adb" +AVD="${FN_AVD:-fnmsg-android-test}" + +# ⚠️ `-no-snapshot-load` is the point of this block, and `-no-snapshot-save` +# keeps the next run clean too. A warm boot is faster and carries the last +# session's account with it. +if ! "$ADB" shell getprop sys.boot_completed 2>/dev/null | grep -q 1; then + echo "### cold-booting $AVD (no snapshot)" + nohup "$ANDROID_HOME/emulator/emulator" -avd "$AVD" \ + -no-snapshot-load -no-snapshot-save -no-boot-anim \ + -netdelay none -netspeed full >/tmp/fnmsg-emu.log 2>&1 & + until "$ADB" shell getprop sys.boot_completed 2>/dev/null | grep -q 1; do sleep 5; done +else + echo "### emulator already up β€” clearing the app rather than trusting it" + "$ADB" uninstall io.flamenet.messenger >/dev/null 2>&1 || true + "$ADB" uninstall io.flamenet.messenger.test >/dev/null 2>&1 || true +fi + +echo "### signed-in account on the device before we start:" +"$ADB" shell pm list packages 2>/dev/null | grep -i flamenet || echo " (nothing installed β€” good)" + +echo "### relay $RELAY Β· classes $CLASSES" +set +e +./gradlew --no-daemon :app:connectedDebugAndroidTest \ + -Pandroid.testInstrumentationRunnerArguments.relay="$RELAY" \ + -Pandroid.testInstrumentationRunnerArguments.class="$CLASSES" 2>&1 \ + | tee /tmp/fnmsg-instrumented.log \ + | grep -E "FAILED|PASSED|SKIPPED|BUILD|Finished [0-9]+ test|Tests on" +STATUS=${PIPESTATUS[0]} +set -e + +# πŸ”΄ **A skip is not a pass.** Every one of these suites opens with +# `assumeTrue(relayReachable())`, so an unreachable relay produces a green +# build that exercised nothing. Say which happened. +SKIPS=$(grep -c "SKIPPED" /tmp/fnmsg-instrumented.log || true) +RAN=$(grep -oE "Finished [0-9]+ test" /tmp/fnmsg-instrumented.log | tail -1 || echo "") +echo +echo "### $RAN, $SKIPS skipped" +if [ "$SKIPS" != "0" ] && [ "$STATUS" = "0" ]; then + echo "### VERDICT: SKIPPED β€” the relay was unreachable. This is NOT a pass." + exit 3 +elif [ "$STATUS" = "0" ]; then + echo "### VERDICT: PASSED on device" +else + echo "### VERDICT: FAILED β€” see /tmp/fnmsg-instrumented.log" + echo "### and app/build/reports/androidTests/connected/debug/index.html" +fi +exit $STATUS