Skip to content

Commit 701fb48

Browse files
Merge remote-tracking branch 'origin/main' into fix/issue-33197
2 parents a1d7a2f + e4d86ab commit 701fb48

57 files changed

Lines changed: 2541 additions & 336 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎apps/server/src/cloud/ManagedEndpointRuntime.test.ts‎

Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -955,6 +955,54 @@ describe("CloudManagedEndpointRuntime", () => {
955955
}),
956956
);
957957

958+
it.effect.each(["SIGINT", "SIGTERM"] as const)(
959+
"does not restart a connector that exits after the server receives %s",
960+
(signal) =>
961+
Effect.gen(function* () {
962+
const spawned: Array<number> = [];
963+
const firstExit = yield* Deferred.make<ChildProcessSpawner.ExitCode>();
964+
const firstKilled = yield* Deferred.make<void>();
965+
const spawner = ChildProcessSpawner.make(() =>
966+
Effect.gen(function* () {
967+
const pid = 400 + spawned.length;
968+
spawned.push(pid);
969+
const handle = makeHandle({
970+
pid,
971+
exitCode:
972+
pid === 400
973+
? Deferred.await(firstExit)
974+
: (Effect.never as Effect.Effect<ChildProcessSpawner.ExitCode>),
975+
onKill: () => {
976+
if (pid === 400) Deferred.doneUnsafe(firstKilled, Exit.void);
977+
},
978+
});
979+
yield* Effect.addFinalizer(() => handle.kill().pipe(Effect.ignore));
980+
return handle;
981+
}),
982+
);
983+
// Invoke only the runtime's own listeners; emitting would reach the test worker's too.
984+
const listenersBefore = new Set(process.listeners(signal));
985+
const runtime = yield* buildCloudManagedEndpointRuntime(spawner);
986+
const runtimeListeners = process
987+
.listeners(signal)
988+
.filter((listener) => !listenersBefore.has(listener));
989+
990+
yield* runtime.applyConfig({
991+
providerKind: "cloudflare_tunnel",
992+
connectorToken: "token",
993+
tunnelId: "tunnel-1",
994+
});
995+
for (const listener of runtimeListeners) listener(signal);
996+
// The process group receives the signal too, so the connector exits cleanly.
997+
yield* Deferred.succeed(firstExit, ChildProcessSpawner.ExitCode(0));
998+
yield* Deferred.await(firstKilled);
999+
// Holding the reconcile lock waits out the supervisor's restart decision.
1000+
yield* runtime.restartConnector(400, Effect.succeed(true));
1001+
1002+
expect(spawned).toEqual([400]);
1003+
}),
1004+
);
1005+
9581006
it.effect("serializes concurrent connector config changes", () =>
9591007
Effect.gen(function* () {
9601008
const spawned: Array<number> = [];

‎apps/server/src/cloud/ManagedEndpointRuntime.ts‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -228,6 +228,22 @@ export const make = Effect.gen(function* () {
228228
const prunedRef = yield* Ref.make(false);
229229
const retryLoopRunningRef = yield* Ref.make(false);
230230
const notReadyChecksBeforeRestartRef = yield* Ref.make(CONNECTOR_NOT_READY_CHECKS_BEFORE_RESTART);
231+
// The relay client shares the server's process group, so it exits on these signals before the finalizer runs.
232+
let shutdownSignalReceived = false;
233+
const onShutdownSignal = () => {
234+
shutdownSignalReceived = true;
235+
};
236+
yield* Effect.acquireRelease(
237+
Effect.sync(() => {
238+
process.on("SIGINT", onShutdownSignal);
239+
process.on("SIGTERM", onShutdownSignal);
240+
}),
241+
() =>
242+
Effect.sync(() => {
243+
process.off("SIGINT", onShutdownSignal);
244+
process.off("SIGTERM", onShutdownSignal);
245+
}),
246+
);
231247
// An explicit cloudflared transport in the environment is the user's choice.
232248
const env = yield* HostProcess.Environment;
233249
const transportChosenByUser = Option.isSome(
@@ -363,6 +379,7 @@ export const make = Effect.gen(function* () {
363379

364380
const desiredConfig = yield* Ref.get(desiredConfigRef);
365381
if (
382+
shutdownSignalReceived ||
366383
!desiredConfig ||
367384
desiredConfig.providerKind !== "cloudflare_tunnel" ||
368385
runtimeConfigKey(desiredConfig) !== connector.configKey

‎apps/server/src/cloud/pinnedRuntime.ts‎

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,8 @@ export class PinnedRuntimeInstallError extends Schema.TaggedError<PinnedRuntimeI
7171
exitCode: Schema.optional(Schema.Number),
7272
stdoutLength: Schema.optional(Schema.Number),
7373
stderrLength: Schema.optional(Schema.Number),
74+
httpStatus: Schema.optional(Schema.Number),
75+
downloadHost: Schema.optional(Schema.String),
7476
cause: Schema.optional(Schema.Defect()),
7577
},
7678
) {
@@ -163,6 +165,13 @@ const fetchReleaseAsset = Effect.fn("cloud.pinned_runtime.fetch_release_asset")(
163165
(cause) =>
164166
new PinnedRuntimeInstallError({
165167
step: cause._tag === "TimeoutError" ? `${step} (timed out)` : step,
168+
httpStatus:
169+
HttpClientError.isHttpClientError(cause) && cause.reason._tag === "StatusCodeError"
170+
? cause.reason.response.status
171+
: undefined,
172+
downloadHost: URL.parse(
173+
HttpClientError.isHttpClientError(cause) ? cause.request.url : url,
174+
)?.host,
166175
cause,
167176
}),
168177
),

‎apps/server/src/cloud/selfUpdate.test.ts‎

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import * as Layer from "effect/Layer";
1111
import * as Path from "effect/Path";
1212
import * as Stream from "effect/Stream";
1313
import * as TestClock from "effect/testing/TestClock";
14-
import { HttpClient, HttpClientResponse } from "effect/http";
14+
import { HttpClient, HttpClientRequest, HttpClientResponse } from "effect/http";
1515
import * as ChildProcessSpawner from "effect/process/ChildProcessSpawner";
1616

1717
import packageJson from "../../package.json" with { type: "json" };
@@ -30,6 +30,7 @@ interface HarnessOptions {
3030
readonly preflightDuration?: Duration.Input;
3131
readonly preflightStarted?: Deferred.Deferred<void>;
3232
readonly beforeDownload?: Effect.Effect<void>;
33+
readonly httpClient?: HttpClient.HttpClient;
3334
readonly requestUpdate?: ServiceLauncherClient.ServiceLauncherClient["Service"]["requestUpdate"];
3435
readonly desktopAppUpdate?: DesktopAppUpdate.DesktopAppUpdate["Service"];
3536
readonly policy?: ManagedPolicyDocument;
@@ -145,7 +146,7 @@ const makeHarness = Effect.fn("test.make_self_update_harness")(function* (
145146
),
146147
Effect.provideService(
147148
HttpClient.HttpClient,
148-
releaseHttpClient(order, urls, options.beforeDownload),
149+
options.httpClient ?? releaseHttpClient(order, urls, options.beforeDownload),
149150
),
150151
Effect.provideService(HostProcess.Platform, "linux"),
151152
Effect.provideService(HostProcess.Architecture, "x64"),
@@ -156,10 +157,45 @@ const makeHarness = Effect.fn("test.make_self_update_harness")(function* (
156157
),
157158
),
158159
);
159-
return { selfUpdate, order, urls };
160+
return { selfUpdate, order, urls, baseDir };
160161
});
161162

162163
it.layer(NodeServices.layer)("server self update", (it) => {
164+
it.effect.each([
165+
{ asset: "checksums", status: 504 },
166+
{ asset: "archive", status: 503 },
167+
{ asset: "checksums", status: 403 },
168+
])("reports an HTTP $status downloading $asset and allows retry", ({ asset, status }) =>
169+
Effect.gen(function* () {
170+
let failDownload = true;
171+
const { selfUpdate, order, baseDir } = yield* makeHarness({
172+
httpClient: releaseHttpClient([], []).pipe(
173+
HttpClient.transform((response, request) =>
174+
failDownload && request.url.endsWith("/SHA256SUMS") === (asset === "checksums")
175+
? Effect.succeed(
176+
HttpClientResponse.fromWeb(
177+
HttpClientRequest.get("https://user:[email protected]/private?token=secret"),
178+
new Response("private response body", { status }),
179+
),
180+
)
181+
: response,
182+
),
183+
),
184+
});
185+
const error = yield* selfUpdate.update({ targetVersion: "1.1.0" }).pipe(Effect.flip);
186+
expect(error.reason).toBe(
187+
`Could not prepare [email protected] while downloading the t3 release ${asset} (HTTP ${status} from cdn.example).`,
188+
);
189+
expect(order).toEqual([]);
190+
const fs = yield* FileSystem.FileSystem;
191+
const path = yield* Path.Path;
192+
expect(yield* fs.readDirectory(path.join(baseDir, "runtime", "versions"))).toEqual([]);
193+
failDownload = false;
194+
expect((yield* selfUpdate.update({ targetVersion: "1.1.0" })).updateId).toBe("launcher-id");
195+
expect(order).toEqual(["extract", "preflight", "accept"]);
196+
}),
197+
);
198+
163199
// regression: #5181 make remote updates rollback-safe
164200
it.effect("rejects invalid versions and desktop-managed servers before staging", () =>
165201
Effect.gen(function* () {

‎apps/server/src/cloud/selfUpdate.ts‎

Lines changed: 21 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -282,16 +282,27 @@ export const make = Effect.fn("cloud.server_self_update.make")(function* () {
282282
),
283283
),
284284
}).pipe(
285-
Effect.mapError((error) =>
286-
error._tag === "PinnedRuntimePreflightBlockedError"
287-
? failWith(error.reason, error)
288-
: mirror !== undefined && isUnpublishedRelease(error)
289-
? failWith(
290-
`${mirror.organization}'s update mirror has no t3@${targetVersion} for this machine.`,
291-
error,
292-
)
293-
: failWith(`Could not prepare t3@${targetVersion}.`, error),
294-
),
285+
Effect.mapError((error) => {
286+
if (error._tag === "PinnedRuntimePreflightBlockedError") {
287+
return failWith(error.reason, error);
288+
}
289+
if (mirror !== undefined && isUnpublishedRelease(error)) {
290+
return failWith(
291+
`${mirror.organization}'s update mirror has no t3@${targetVersion} for this machine.`,
292+
error,
293+
);
294+
}
295+
const downloadFailure = [
296+
error.httpStatus === undefined ? undefined : `HTTP ${error.httpStatus}`,
297+
error.downloadHost === undefined ? undefined : `from ${error.downloadHost}`,
298+
]
299+
.filter((part) => part !== undefined)
300+
.join(" ");
301+
return failWith(
302+
`Could not prepare t3@${targetVersion} while ${error.step}${downloadFailure ? ` (${downloadFailure})` : ""}.`,
303+
error,
304+
);
305+
}),
295306
);
296307

297308
yield* reportProgress("installing");

0 commit comments

Comments
 (0)