Public Access
Let a failed update check say so, instead of reporting good news
The phone reported every build as current because the release repository is private. Gitea answers 404 rather than 403 for a repo you cannot see, the client reads that address anonymously, and AndroidUpdateChannel caught the failure and returned null — which IUpdateChannel documented as meaning "this build is the latest". The check had never once succeeded on any phone and nothing anywhere said so. Two faults, and the second is why the first lasted. The seam said null was the honest answer for an unreachable channel, on the reasoning that the caller does the same thing either way. That is true of the six-hourly pass and false of CHECK NOW. UpdateViewModel already draws the line correctly — silent on the timer, the exception's message on the button — and it could only ever draw the first half, because nothing was ever thrown at it. The desktop's channel does not catch, so the interface described neither implementation. So CheckAsync throws now, and null means one thing. A release that is reachable but missing its manifest or the APK it names throws too: "you are up to date" about a half-published feed is the same lie in a smaller costume, and the self-healing that argument protected is untouched, since the timer still swallows everything. The precondition is written down where somebody would look, rather than left as a sentence about where a token could live. ADR 0013 §4 already said a private release repository was incompatible with this design; nobody checked which side of it this repository was on. It is one curl, and manual-checks phase 16 now opens with it — pointedly not against /api/v1/version, which answers 200 from a forge that is up whatever is readable on it, and which is what made this look like nothing was wrong. Phone check 17.4 was the one that passed all along. It now presses CHECK NOW with the network off as well as on, because two different answers are the whole of what makes the first one worth reading.
This commit is contained in:
@@ -132,51 +132,75 @@ internal sealed class AndroidUpdateChannel : IUpdateChannel
|
||||
/// <summary>What Android thinks is installed, which is the number the comparison is made on.</summary>
|
||||
private long InstalledVersionCode { get; }
|
||||
|
||||
/// <inheritdoc />
|
||||
/// <summary>
|
||||
/// Asks the channel's release what it is publishing.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// ◆ <b>Nothing here is caught, and it used to catch everything.</b> Every network and parse failure
|
||||
/// resolved to null on the argument that an unreachable forge is a phone on a train — which is a good
|
||||
/// argument for the timer and no argument at all for the button. <c>UpdateViewModel</c> already draws
|
||||
/// that line correctly: a background pass swallows and goes back to Idle, a pressed CHECK NOW reports
|
||||
/// the message. Swallowing here took the second half away and answered every failure with "you are on
|
||||
/// the latest build".
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// <b>What that hid was the whole feature not working.</b> The release repository was private, so every
|
||||
/// request answered 404 — Gitea does not distinguish "not there" from "not yours" — and every phone
|
||||
/// reported itself current. The check had never once succeeded and nothing anywhere said so. The feed
|
||||
/// has to be readable without credentials; see the interface, which now records that as a precondition.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// A release that is <em>reachable</em> but missing the manifest or the APK it names throws too, and
|
||||
/// that is deliberate rather than incidental. It means CI published half a release, which is a fact
|
||||
/// worth an answer — "you are up to date" about a broken feed is the same lie in a smaller costume. The
|
||||
/// self-healing that argument protected is unaffected: the timer swallows it, so a half-published
|
||||
/// release still fixes itself without anybody being told.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <exception cref="HttpRequestException">The forge could not be reached, or refused.</exception>
|
||||
/// <exception cref="InvalidOperationException">The release is there and does not carry a usable build.</exception>
|
||||
public async Task<AvailableUpdate?> CheckAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
// Every failure below resolves to null rather than throwing, and the caller's remark says why: an
|
||||
// unreachable forge is a phone on a train. It is not news and it heals itself in six hours.
|
||||
try
|
||||
var release = await ReadAsync(ReleaseUrl(), ForgeJsonContext.Default.ForgeRelease, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (Asset(release, $"android-{channel}.json") is not { } manifestAsset)
|
||||
{
|
||||
var release = await ReadAsync(ReleaseUrl(), ForgeJsonContext.Default.ForgeRelease, cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (Asset(release, $"android-{channel}.json") is not { } manifestAsset)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var manifest = await ReadAsync(
|
||||
manifestAsset,
|
||||
ForgeJsonContext.Default.AndroidChannelManifest,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (manifest is null || manifest.VersionCode <= InstalledVersionCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Asset(release, manifest.Apk) is not { } apk)
|
||||
{
|
||||
// A manifest naming an APK the release does not carry. CI uploads the package before the
|
||||
// manifest precisely so this window is short, and answering null rather than throwing is
|
||||
// what makes a half-published release a thing that fixes itself.
|
||||
return null;
|
||||
}
|
||||
|
||||
found = (manifest.VersionName, apk);
|
||||
|
||||
return new AvailableUpdate(manifest.VersionName);
|
||||
throw new InvalidOperationException(
|
||||
$"The {channel} release carries no android-{channel}.json, so there is nothing saying "
|
||||
+ "what it publishes.");
|
||||
}
|
||||
catch (Exception exception) when (exception is HttpRequestException
|
||||
or JsonException
|
||||
or TaskCanceledException
|
||||
or IOException)
|
||||
|
||||
var manifest = await ReadAsync(
|
||||
manifestAsset,
|
||||
ForgeJsonContext.Default.AndroidChannelManifest,
|
||||
cancellationToken)
|
||||
.ConfigureAwait(false);
|
||||
|
||||
if (manifest is null)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The {channel} release's android-{channel}.json could not be read as a manifest.");
|
||||
}
|
||||
|
||||
// The one place null is returned, and it means what null is documented to mean: this build is
|
||||
// current. Compared on the version code because that is the number Android itself accepts or
|
||||
// refuses an install on — see AndroidChannelManifest.
|
||||
if (manifest.VersionCode <= InstalledVersionCode)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
if (Asset(release, manifest.Apk) is not { } apk)
|
||||
{
|
||||
throw new InvalidOperationException(
|
||||
$"The {channel} release advertises {manifest.VersionName} but carries no {manifest.Apk}.");
|
||||
}
|
||||
|
||||
found = (manifest.VersionName, apk);
|
||||
|
||||
return new AvailableUpdate(manifest.VersionName);
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
|
||||
Reference in New Issue
Block a user