Skip to content
Open
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
66 changes: 63 additions & 3 deletions apps/desktop/src/updates/DesktopUpdates.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ interface UpdatesHarnessOptions {
readonly setUpdateChannelError?: DesktopAppSettings.DesktopSettingsWriteError;
readonly setDisableDifferentialDownload?: Effect.Effect<void>;
readonly stopBackend?: Effect.Effect<void>;
readonly quitAndInstall?: Effect.Effect<void, ElectronUpdater.ElectronUpdaterQuitAndInstallError>;
readonly env?: Record<string, string | undefined>;
}

Expand All @@ -43,6 +44,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
const feedUrls: ElectronUpdater.ElectronUpdaterFeedUrl[] = [];
const listeners = new Map<string, Set<(...args: readonly unknown[]) => void>>();
const sentStates: DesktopUpdateState[] = [];
const installSteps: string[] = [];

const addListener = (eventName: string, listener: (...args: readonly unknown[]) => void) => {
const eventListeners = listeners.get(eventName) ?? new Set();
Expand Down Expand Up @@ -84,7 +86,10 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
checkCount += 1;
}).pipe(Effect.andThen(options.checkForUpdates ?? Effect.void)),
downloadUpdate: Effect.void,
quitAndInstall: () => Effect.void,
quitAndInstall: () =>
Effect.sync(() => {
installSteps.push("quitAndInstall");
}).pipe(Effect.andThen(options.quitAndInstall ?? Effect.void)),
on: (eventName, listener) =>
Effect.acquireRelease(
Effect.sync(() => {
Expand All @@ -109,14 +114,18 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
Effect.sync(() => {
sentStates.push(state as DesktopUpdateState);
}),
destroyAll: Effect.void,
destroyAll: Effect.sync(() => {
installSteps.push("destroyAll");
}),
syncAllAppearance: () => Effect.void,
} satisfies ElectronWindow.ElectronWindow["Service"]);

const stubBackendInstance: DesktopBackendPool.DesktopBackendInstance = {
id: DesktopBackendPool.PRIMARY_INSTANCE_ID,
label: Effect.succeed("Windows"),
start: Effect.void,
start: Effect.sync(() => {
installSteps.push("startBackend");
}),
stop: () => options.stopBackend ?? Effect.void,
currentConfig: Effect.succeed(Option.none()),
snapshot: Effect.succeed({
Expand Down Expand Up @@ -219,6 +228,7 @@ function makeHarness(options: UpdatesHarnessOptions = {}) {
0,
),
sentStates,
installSteps,
emit: (eventName: string, payload?: unknown) => {
for (const listener of listeners.get(eventName) ?? []) {
listener(payload);
Expand Down Expand Up @@ -725,6 +735,56 @@ describe("DesktopUpdates", () => {
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("destroys windows only after quitAndInstall starts", () => {
const harness = makeHarness();

return Effect.scoped(
Effect.gen(function* () {
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;

const result = yield* updates.install;
assert.isTrue(result.accepted);
assert.deepEqual(harness.installSteps, ["quitAndInstall", "destroyAll"]);
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("keeps windows when quitAndInstall fails", () => {
const harness = makeHarness({
quitAndInstall: Effect.fail(
new ElectronUpdater.ElectronUpdaterQuitAndInstallError({
channel: null,
isSilent: true,
isForceRunAfter: true,
cause: new Error("installer refused"),
}),
),
});

return Effect.scoped(
Effect.gen(function* () {
const desktopState = yield* DesktopState.DesktopState;
const updates = yield* DesktopUpdates.DesktopUpdates;
yield* updates.configure;
harness.emit("update-downloaded", { version: "1.2.4" });
yield* flushCallbacks;

const result = yield* updates.install;
assert.isTrue(result.accepted);
assert.isFalse(result.completed);
assert.isFalse(yield* Ref.get(desktopState.quitting));
assert.deepEqual(harness.installSteps, ["quitAndInstall", "startBackend"]);

const failedState = yield* updates.getState;
assert.equal(failedState.status, "downloaded");
assert.equal(failedState.errorContext, "install");
}),
).pipe(Effect.provide(Layer.merge(TestClock.layer(), harness.layer)));
});

it.effect("persists channel changes through the settings service", () => {
const harness = makeHarness();

Expand Down
12 changes: 10 additions & 2 deletions apps/desktop/src/updates/DesktopUpdates.ts
Original file line number Diff line number Diff line change
Expand Up @@ -483,6 +483,11 @@ export const make = Effect.gen(function* () {

yield* Ref.set(desktopState.quitting, true);

const instances = yield* pool.list;
const restartStoppedBackends = Effect.forEach(instances, (instance) => instance.start, {
concurrency: "unbounded",
}).pipe(Effect.asVoid);

return yield* Effect.gen(function* () {
// Stop every backend in the pool, not just the primary. With
// parallel WSL + Windows backends, leaving the WSL instance up
Expand All @@ -491,23 +496,25 @@ export const make = Effect.gen(function* () {
// WSL child gets hard-killed by the OS instead of receiving
// SIGTERM + grace. Stops run concurrently with the same 5s
// budget the primary had on its own.
const instances = yield* pool.list;
yield* Effect.forEach(
instances,
(instance) => instance.stop({ timeout: Duration.seconds(5) }),
{ concurrency: "unbounded" },
);
yield* electronWindow.destroyAll;
yield* electronUpdater.quitAndInstall({
isSilent: true,
isForceRunAfter: true,
});
// Close windows only after quitAndInstall has started. If install
// fails, the user still has a window.
yield* electronWindow.destroyAll;
Comment thread
cursor[bot] marked this conversation as resolved.
return { accepted: true, completed: false };
}).pipe(
Effect.catchTags({
ElectronUpdaterQuitAndInstallError: Effect.fn("desktop.updates.handleInstallFailure")(
function* (error) {
yield* resetInstallAction;
yield* restartStoppedBackends;
yield* updateState((current) =>
reduceDesktopUpdateStateOnInstallFailure(current, error.message),
);
Expand All @@ -528,6 +535,7 @@ export const make = Effect.gen(function* () {
return yield* Effect.failCause(cause);
}
yield* resetInstallAction;
yield* restartStoppedBackends;
const error = new DesktopUpdateUnexpectedActionError({ action: "install", cause });
yield* updateState((current) =>
reduceDesktopUpdateStateOnInstallFailure(current, error.message),
Expand Down
Loading