Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
24 changes: 22 additions & 2 deletions apps/server/src/cloud/selfUpdate.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,17 +30,26 @@ interface HarnessOptions {
readonly preflightDuration?: Duration.Input;
readonly preflightStarted?: Deferred.Deferred<void>;
readonly beforeDownload?: Effect.Effect<void>;
readonly checksumsStatus?: number;
readonly requestUpdate?: ServiceLauncherClient.ServiceLauncherClient["Service"]["requestUpdate"];
readonly desktopAppUpdate?: DesktopAppUpdate.DesktopAppUpdate["Service"];
readonly policy?: ManagedPolicyDocument;
}

const archiveBytes = new TextEncoder().encode("not really a tarball");
const releaseHttpClient = (order: string[], urls: string[], beforeDownload?: Effect.Effect<void>) =>
const releaseHttpClient = (
order: string[],
urls: string[],
beforeDownload?: Effect.Effect<void>,
checksumsStatus?: number,
) =>
HttpClient.make((request) =>
Effect.gen(function* () {
urls.push(request.url);
if (request.url.endsWith("/SHA256SUMS")) {
if (checksumsStatus !== undefined) {
return HttpClientResponse.fromWeb(request, new Response("", { status: checksumsStatus }));
}
const digest = yield* Effect.promise(() => crypto.subtle.digest("SHA-256", archiveBytes));
const hex = Array.from(new Uint8Array(digest), (byte) =>
byte.toString(16).padStart(2, "0"),
Expand Down Expand Up @@ -145,7 +154,7 @@ const makeHarness = Effect.fn("test.make_self_update_harness")(function* (
),
Effect.provideService(
HttpClient.HttpClient,
releaseHttpClient(order, urls, options.beforeDownload),
releaseHttpClient(order, urls, options.beforeDownload, options.checksumsStatus),
),
Effect.provideService(HostProcess.Platform, "linux"),
Effect.provideService(HostProcess.Architecture, "x64"),
Expand Down Expand Up @@ -257,6 +266,17 @@ it.layer(NodeServices.layer)("server self update", (it) => {
}),
);

it.effect("names the failed step, HTTP status, and host when a release download fails", () =>
Effect.gen(function* () {
const { selfUpdate, order } = yield* makeHarness({ checksumsStatus: 504 });
const { reason } = yield* selfUpdate.update({ targetVersion: "1.1.0" }).pipe(Effect.flip);
expect(reason).toContain("checksums");
expect(reason).toContain("504");
expect(reason).toContain("github.com");
expect(order).toEqual([]);
}),
);

it.effect("allows only one update at a time", () =>
Effect.gen(function* () {
const requested = yield* Deferred.make<void>();
Expand Down
28 changes: 26 additions & 2 deletions apps/server/src/cloud/selfUpdate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import {
type ServerSelfUpdateResult,
} from "@t3tools/contracts";
import * as HostProcess from "@t3tools/shared/HostProcess";
import * as Cause from "effect/Cause";
import * as Config from "effect/Config";
import * as Context from "effect/Context";
import * as Duration from "effect/Duration";
Expand All @@ -18,7 +19,7 @@ import * as Layer from "effect/Layer";
import * as Path from "effect/Path";
import * as Queue from "effect/Queue";
import * as Stream from "effect/Stream";
import { HttpClient } from "effect/http";
import { HttpClient, HttpClientError } from "effect/http";

import {
CLI_RELEASE_BASE_URL_ENV,
Expand Down Expand Up @@ -122,6 +123,29 @@ export function trackSwitchBlockReason(input: {
: null;
}

/** Names the failed install step and, for a download, the host and its answer, never the URL. */
const prepareFailureReason = (targetVersion: string, error: PinnedRuntimeInstallError) => {
const failed = `Could not prepare t3@${targetVersion} while ${error.step}`;
const cause = error.cause;
if (!HttpClientError.isHttpClientError(cause)) {
return Cause.isTimeoutError(cause) ? `${failed}. Try again.` : `${failed}.`;
}
const host = URL.parse(cause.request.url)?.host ?? "the release host";
switch (cause.reason._tag) {
case "StatusCodeError": {
const status = cause.reason.response.status;
const retryable = status >= 500 || status === 408 || status === 429;
return `${failed}: ${host} answered HTTP ${status}.${retryable ? " Try again." : ""}`;
}
// A refused connection, or one dropped while the body streams in.
case "TransportError":
case "DecodeError":
return `${failed}: the connection to ${host} failed. Try again.`;
default:
return `${failed} from ${host}.`;
}
};

export const make = Effect.fn("cloud.server_self_update.make")(function* () {
const serverConfig = yield* ServerConfig.ServerConfig;
const desktopAppUpdate = yield* DesktopAppUpdate.DesktopAppUpdate;
Expand Down Expand Up @@ -290,7 +314,7 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () {
`${mirror.organization}'s update mirror has no t3@${targetVersion} for this machine.`,
error,
)
: failWith(`Could not prepare t3@${targetVersion}.`, error),
: failWith(prepareFailureReason(targetVersion, error), error),
),
);

Expand Down
Loading