Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
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
484 changes: 474 additions & 10 deletions apps/desktop/src/updates/DesktopUpdates.test.ts

Large diffs are not rendered by default.

98 changes: 78 additions & 20 deletions apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -351,6 +351,9 @@ function isArm64HostRunningIntelBuild(runtimeInfo: DesktopRuntimeInfo): boolean
return runtimeInfo.hostArch === "arm64" && runtimeInfo.appArch === "x64";
}

export const checkpointRequestedMessage =
"Checkpoint requested. Check again later to pick up the result.";

/** @public Service construction is part of the canonical Effect module API. */
export const make = Effect.gen(function* () {
const config = yield* DesktopConfig.DesktopConfig;
Expand Down Expand Up @@ -538,19 +541,60 @@ export const make = Effect.gen(function* () {
const state = yield* Ref.get(updateStateRef);
const checkedAt = yield* currentIsoTimestamp;
yield* setState(reduceDesktopUpdateStateOnCheckStart(state, checkedAt));
return yield* localUpdates.inspect(environment.appVersion).pipe(
const requestCheckpoint = reason === "web-ui" || reason === "menu";
return yield* localUpdates.inspect(environment.appVersion, requestCheckpoint).pipe(
Effect.flatMap((inspection) => {
const checkpointRequested =
inspection.status === "checkpoint-requested" || inspection.checkpointRequested;
const result = { checked: true, checkpointRequested, error: null };
const preserveFailure =
state.canRetry &&
((state.status === "error" && state.errorContext === "download") ||
(state.downloadedVersion !== null && state.errorContext === "install"));
const applyRequest = (next: DesktopUpdateState): DesktopUpdateState =>
checkpointRequested && next.status !== "error" && next.errorContext !== "install"
? {
...next,
status: next.status === "up-to-date" ? "idle" : next.status,
message: checkpointRequestedMessage,
}
: next;
if (inspection.status === "checkpoint-requested") {
const pending =
state.status === "error" && !preserveFailure
? {
...state,
status: "idle" as const,
checkedAt,
message: null,
errorContext: null,
canRetry: false,
}
: { ...state, checkedAt };
return setState(applyRequest(pending)).pipe(Effect.as(result));
}
if (inspection.status === "up-to-date") {
return Ref.set(localCheckpointTagRef, Option.none()).pipe(
Effect.andThen(setState(reduceDesktopUpdateStateOnNoUpdate(state, checkedAt))),
Effect.as(true),
Effect.andThen(
setState(applyRequest(reduceDesktopUpdateStateOnNoUpdate(state, checkedAt))),
),
Effect.as(result),
);
}
const releaseNotes = mapLastCodeLocalReleaseNotes(inspection);
const build =
inspection.build?.checkpointTag === inspection.checkpointTag
? inspection.build
: undefined;
// A verified external build clears a build failure, but an install failure
// still needs its retry action even when the same package is rediscovered.
if (
preserveFailure &&
state.availableVersion === inspection.availableVersion &&
(state.errorContext === "install" || !build)
) {
return setState({ ...state, checkedAt }).pipe(Effect.as(result));
}
const releaseNotes = mapLastCodeLocalReleaseNotes(inspection);
const available = reduceDesktopUpdateStateOnUpdateAvailable(
{ ...state, downloadedVersion: null },
inspection.availableVersion,
Expand All @@ -566,18 +610,27 @@ export const make = Effect.gen(function* () {
),
Effect.andThen(
setState(
build
? reduceDesktopUpdateStateOnDownloadComplete(available, inspection.availableVersion)
: available,
applyRequest(
build
? reduceDesktopUpdateStateOnDownloadComplete(
available,
inspection.availableVersion,
)
: available,
),
),
),
Effect.as(true),
Effect.as(result),
);
}),
Effect.catchTag("LastCodeLocalUpdateError", (error) =>
setState(reduceDesktopUpdateStateOnCheckFailure(state, error.message, checkedAt)).pipe(
Effect.as(true),
),
setState(
state.status === "available" ||
state.downloadedVersion !== null ||
(state.status === "error" && state.errorContext === "download" && state.canRetry)
? { ...state, checkedAt }
: reduceDesktopUpdateStateOnCheckFailure(state, error.message, checkedAt),
).pipe(Effect.as({ checked: true, checkpointRequested: false, error: error.message })),
),
);
});
Expand All @@ -587,22 +640,25 @@ export const make = Effect.gen(function* () {
actionReservation: "acquire" | "held" = "acquire",
) {
yield* Effect.annotateCurrentSpan({ reason });
if (yield* Ref.get(desktopState.quitting)) return false;
if (!(yield* Ref.get(updaterConfiguredRef))) return false;
if (yield* Ref.get(desktopState.quitting))
return { checked: false, checkpointRequested: false, error: null };
if (!(yield* Ref.get(updaterConfiguredRef)))
return { checked: false, checkpointRequested: false, error: null };

const state = yield* Ref.get(updateStateRef);
if (state.status === "downloading") {
yield* logUpdaterInfo("skipping update check while update is active", {
reason,
status: state.status,
});
return false;
return { checked: false, checkpointRequested: false, error: null };
}

if (state.source === "lastcode-local" && (!state.enabled || !localUpdates.supported)) {
return false;
return { checked: false, checkpointRequested: false, error: null };
}
if (actionReservation === "acquire" && !(yield* tryStartUpdateAction("check"))) return false;
if (actionReservation === "acquire" && !(yield* tryStartUpdateAction("check")))
return { checked: false, checkpointRequested: false, error: null };

if (state.source === "lastcode-local") {
const check = checkForLocalUpdate(reason);
Expand All @@ -617,7 +673,7 @@ export const make = Effect.gen(function* () {
yield* logUpdaterInfo("checking for updates", { reason });

return yield* electronUpdater.checkForUpdates.pipe(
Effect.as(true),
Effect.as({ checked: true, checkpointRequested: false, error: null }),
Effect.catchTags({
ElectronUpdaterCheckForUpdatesError: Effect.fn(
"desktop.updates.handleCheckForUpdatesFailure",
Expand All @@ -630,7 +686,7 @@ export const make = Effect.gen(function* () {
errorTag: error._tag,
channel: error.channel,
});
return true;
return { checked: true, checkpointRequested: false, error: error.message };
}),
}),
);
Expand Down Expand Up @@ -1439,12 +1495,14 @@ export const make = Effect.gen(function* () {
if (!(yield* Ref.get(updaterConfiguredRef))) {
return {
checked: false,
checkpointRequested: false,
error: null,
state: yield* Ref.get(updateStateRef),
};
}
const checked = yield* checkForUpdates(reason);
const result = yield* checkForUpdates(reason);
return {
checked,
...result,
state: yield* Ref.get(updateStateRef),
};
}),
Expand Down
11 changes: 11 additions & 0 deletions apps/desktop/src/updates/LastCodeLocalUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,17 @@ describe("LastCodeLocalUpdates", () => {
]);
});

it("requests a checkpoint only when explicitly asked", () => {
assert.deepEqual(groupedInspectionArgs("1.2.3-nightly.4", true), [
"--current-version",
"1.2.3-nightly.4",
"--release-notes-format",
"grouped-v1",
"--request-checkpoint",
]);
assert.notInclude(groupedInspectionArgs("1.2.3-nightly.4"), "--request-checkpoint");
});

it("parses the helper's final structured result", () => {
assert.deepEqual(
parseHelperResult(
Expand Down
24 changes: 20 additions & 4 deletions apps/desktop/src/updates/LastCodeLocalUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -238,15 +238,21 @@ const BuildResult = Schema.Struct({
export type LastCodeLocalUpdateBuild = typeof BuildResult.Type;

const InspectionResult = Schema.Union([
Schema.Struct({
schemaVersion: Schema.Literal(2),
status: Schema.Literal("checkpoint-requested"),
}),
Schema.Struct({
schemaVersion: Schema.Literal(2),
status: Schema.Literal("up-to-date"),
checkpointRequested: Schema.Boolean,
checkpointTag: Schema.String,
availableVersion: Schema.String,
}),
Schema.Struct({
schemaVersion: Schema.Literal(2),
status: Schema.Literal("available"),
checkpointRequested: Schema.Boolean,
checkpointTag: Schema.String,
availableVersion: Schema.String,
build: Schema.optional(BuildResult),
Expand Down Expand Up @@ -291,6 +297,7 @@ export class LastCodeLocalUpdates extends Context.Service<
readonly buildLogPath: string;
readonly inspect: (
currentVersion: string,
requestCheckpoint?: boolean,
) => Effect.Effect<LastCodeLocalUpdateInspection, LastCodeLocalUpdateError>;
readonly build: (
checkpointTag: string,
Expand Down Expand Up @@ -319,8 +326,17 @@ export function usesDetachedHelperProcessGroup(platform: NodeJS.Platform): boole
return platform !== "win32";
}

export function groupedInspectionArgs(currentVersion: string): ReadonlyArray<string> {
return ["--current-version", currentVersion, "--release-notes-format", "grouped-v1"];
export function groupedInspectionArgs(
currentVersion: string,
requestCheckpoint = false,
): ReadonlyArray<string> {
return [
"--current-version",
currentVersion,
"--release-notes-format",
"grouped-v1",
...(requestCheckpoint ? ["--request-checkpoint"] : []),
];
}

export function terminateHelperProcess(
Expand Down Expand Up @@ -640,8 +656,8 @@ function makeLive(environment: DesktopEnvironment.DesktopEnvironment["Service"])
environment.platform === "darwin" &&
environment.runtimeInfo.hostArch === "arm64",
buildLogPath,
inspect: (currentVersion) =>
runHelper("inspect", groupedInspectionArgs(currentVersion)).pipe(
inspect: (currentVersion, requestCheckpoint) =>
runHelper("inspect", groupedInspectionArgs(currentVersion, requestCheckpoint)).pipe(
Effect.flatMap((result) =>
Effect.try({
try: () => decodeInspectionResult(result),
Expand Down
15 changes: 9 additions & 6 deletions apps/desktop/src/updates/updatesTestHarness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,7 @@ export interface UpdatesHarnessOptions {
readonly packageType?: string | undefined;
readonly localNightliesEnabled?: boolean;
readonly localInspection?: LastCodeLocalUpdates.LastCodeLocalUpdateInspection;
readonly localInspect?: (
currentVersion: string,
) => Effect.Effect<LastCodeLocalUpdates.LastCodeLocalUpdateInspection>;
readonly localInspect?: LastCodeLocalUpdates.LastCodeLocalUpdates["Service"]["inspect"];
readonly localBuild?: LastCodeLocalUpdates.LastCodeLocalUpdateBuild;
readonly localBuildEffect?: LastCodeLocalUpdates.LastCodeLocalUpdates["Service"]["build"];
readonly localPrepareInstall?: (
Expand Down Expand Up @@ -122,7 +120,12 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
downloadCount += 1;
if (options.localNightliesEnabled) {
for (const listener of listeners.get("update-downloaded") ?? []) {
listener({ version: options.localInspection?.availableVersion });
listener({
version:
options.localInspection?.status === "available"
? options.localInspection.availableVersion
: undefined,
});
}
}
}).pipe(Effect.andThen(options.downloadUpdate ?? Effect.void)),
Expand Down Expand Up @@ -300,9 +303,9 @@ export function makeHarness(options: UpdatesHarnessOptions = {}) {
const localUpdatesLayer = LastCodeLocalUpdates.layerTest({
supported: options.localNightliesEnabled ?? false,
buildLogPath: `/tmp/t3-desktop-updates-home-${process.pid}/.lastcode/local-updates/build.log`,
inspect: (currentVersion) =>
inspect: (currentVersion, requestCheckpoint) =>
options.localInspect
? options.localInspect(currentVersion)
? options.localInspect(currentVersion, requestCheckpoint)
: options.localInspection
? Effect.succeed(options.localInspection)
: Effect.die("unexpected local update inspection"),
Expand Down
Loading